hydration.spec.ts 225 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525652665276528652965306531653265336534653565366537653865396540654165426543654465456546654765486549655065516552655365546555655665576558655965606561656265636564656565666567656865696570657165726573657465756576657765786579658065816582658365846585658665876588658965906591659265936594659565966597659865996600660166026603660466056606660766086609661066116612661366146615661666176618661966206621662266236624662566266627662866296630663166326633663466356636663766386639664066416642664366446645664666476648664966506651665266536654665566566657665866596660666166626663666466656666666766686669667066716672667366746675667666776678667966806681668266836684668566866687668866896690669166926693669466956696669766986699670067016702670367046705670667076708670967106711671267136714671567166717671867196720672167226723672467256726672767286729673067316732673367346735673667376738673967406741674267436744674567466747674867496750675167526753675467556756675767586759676067616762676367646765676667676768676967706771677267736774677567766777677867796780678167826783678467856786678767886789679067916792679367946795679667976798679968006801680268036804680568066807680868096810681168126813681468156816681768186819682068216822682368246825682668276828682968306831683268336834683568366837683868396840684168426843684468456846684768486849685068516852685368546855685668576858685968606861686268636864686568666867686868696870687168726873687468756876687768786879688068816882688368846885688668876888688968906891689268936894689568966897689868996900690169026903690469056906690769086909691069116912691369146915691669176918691969206921692269236924692569266927692869296930693169326933693469356936693769386939694069416942694369446945694669476948694969506951695269536954695569566957695869596960696169626963696469656966696769686969697069716972697369746975697669776978697969806981698269836984698569866987698869896990699169926993699469956996699769986999700070017002700370047005700670077008700970107011701270137014701570167017701870197020702170227023702470257026702770287029703070317032703370347035703670377038703970407041704270437044704570467047704870497050705170527053705470557056705770587059706070617062706370647065706670677068706970707071707270737074707570767077707870797080708170827083708470857086708770887089709070917092709370947095709670977098709971007101710271037104710571067107710871097110711171127113711471157116711771187119712071217122712371247125712671277128712971307131713271337134713571367137713871397140714171427143714471457146714771487149715071517152715371547155715671577158715971607161716271637164716571667167716871697170717171727173717471757176717771787179718071817182718371847185718671877188718971907191719271937194719571967197719871997200720172027203720472057206720772087209721072117212721372147215721672177218721972207221722272237224722572267227722872297230723172327233723472357236723772387239724072417242724372447245724672477248724972507251725272537254725572567257725872597260726172627263726472657266726772687269727072717272727372747275727672777278727972807281728272837284728572867287728872897290729172927293729472957296729772987299730073017302730373047305730673077308730973107311731273137314731573167317731873197320732173227323732473257326732773287329733073317332733373347335733673377338733973407341734273437344734573467347734873497350735173527353735473557356735773587359736073617362736373647365736673677368736973707371737273737374737573767377737873797380738173827383738473857386738773887389739073917392739373947395739673977398739974007401740274037404740574067407740874097410741174127413741474157416741774187419742074217422742374247425742674277428742974307431743274337434743574367437743874397440744174427443744474457446744774487449745074517452745374547455745674577458745974607461746274637464746574667467746874697470747174727473747474757476747774787479748074817482748374847485748674877488748974907491749274937494749574967497749874997500750175027503750475057506750775087509751075117512751375147515751675177518751975207521752275237524752575267527752875297530753175327533753475357536753775387539754075417542754375447545754675477548754975507551755275537554755575567557755875597560756175627563756475657566756775687569757075717572757375747575757675777578757975807581758275837584758575867587758875897590759175927593759475957596759775987599760076017602760376047605760676077608760976107611761276137614761576167617761876197620762176227623762476257626762776287629763076317632763376347635763676377638763976407641764276437644764576467647764876497650765176527653765476557656765776587659766076617662766376647665766676677668766976707671767276737674767576767677767876797680768176827683768476857686768776887689769076917692769376947695769676977698769977007701770277037704770577067707770877097710771177127713771477157716771777187719772077217722772377247725772677277728772977307731773277337734773577367737773877397740774177427743774477457746774777487749775077517752775377547755775677577758775977607761776277637764776577667767776877697770777177727773777477757776777777787779778077817782778377847785778677877788778977907791779277937794779577967797779877997800780178027803780478057806780778087809781078117812781378147815781678177818781978207821782278237824782578267827782878297830783178327833783478357836783778387839784078417842784378447845784678477848784978507851785278537854785578567857785878597860786178627863786478657866786778687869787078717872787378747875787678777878787978807881788278837884788578867887788878897890789178927893789478957896789778987899790079017902790379047905790679077908790979107911791279137914791579167917791879197920792179227923792479257926792779287929793079317932793379347935793679377938793979407941794279437944794579467947794879497950795179527953795479557956795779587959796079617962796379647965796679677968796979707971797279737974797579767977797879797980798179827983798479857986798779887989799079917992799379947995799679977998799980008001800280038004800580068007800880098010801180128013801480158016801780188019802080218022802380248025802680278028802980308031803280338034803580368037803880398040804180428043804480458046804780488049805080518052805380548055805680578058805980608061806280638064806580668067806880698070807180728073807480758076807780788079808080818082808380848085808680878088808980908091809280938094809580968097809880998100810181028103810481058106810781088109811081118112811381148115811681178118811981208121812281238124812581268127812881298130813181328133813481358136813781388139814081418142814381448145814681478148814981508151815281538154815581568157815881598160816181628163816481658166816781688169817081718172817381748175817681778178817981808181818281838184818581868187818881898190819181928193819481958196819781988199820082018202820382048205820682078208
  1. import {
  2. VaporTeleport,
  3. child,
  4. createComponent,
  5. createPlainElement,
  6. createVaporSSRApp,
  7. defineVaporAsyncComponent,
  8. defineVaporComponent,
  9. delegateEvents,
  10. renderEffect,
  11. setStyle,
  12. setText,
  13. template,
  14. useVaporCssVars,
  15. } from '../src'
  16. import {
  17. createSSRApp,
  18. defineAsyncComponent,
  19. defineComponent,
  20. h,
  21. nextTick,
  22. reactive,
  23. ref,
  24. withDirectives,
  25. } from '@vue/runtime-dom'
  26. import { isString } from '@vue/shared'
  27. import type { VaporComponentInstance } from '../src/component'
  28. import type { TeleportFragment } from '../src/components/Teleport'
  29. import { VueServerRenderer, compile, runtimeDom, runtimeVapor } from './_utils'
  30. import { setIsHydratingEnabled } from '../src/dom/hydration'
  31. const formatHtml = (raw: string) => {
  32. return raw
  33. .replace(/<!--\[/g, '\n<!--[')
  34. .replace(/]-->/g, ']-->\n')
  35. .replace(/<!--teleport (start|end)-->/g, '\n<!--teleport $1-->\n')
  36. .replace(/\n{2,}/g, '\n')
  37. }
  38. async function testWithVaporApp(
  39. code: string,
  40. components?: Record<string, string | { code: string; vapor: boolean }>,
  41. data?: any,
  42. ) {
  43. return testHydration(code, components, data, {
  44. isVaporApp: true,
  45. interop: true,
  46. })
  47. }
  48. async function testWithVDOMApp(
  49. code: string,
  50. components?: Record<string, string | { code: string; vapor: boolean }>,
  51. data?: any,
  52. ) {
  53. return testHydration(code, components, data, {
  54. isVaporApp: false,
  55. interop: true,
  56. })
  57. }
  58. function compileVaporComponent(
  59. code: string,
  60. data: runtimeDom.Ref<any> = ref({}),
  61. components?: Record<string, any>,
  62. ssr = false,
  63. ) {
  64. if (!code.includes(`<script`)) {
  65. code = `<template>${code}</template>`
  66. }
  67. return compile(code, data, components, {
  68. vapor: true,
  69. ssr,
  70. })
  71. }
  72. async function mountWithHydration(
  73. html: string,
  74. code: string,
  75. data: runtimeDom.Ref<any> = ref({}),
  76. components?: Record<string, any>,
  77. ) {
  78. const container = document.createElement('div')
  79. container.innerHTML = html
  80. document.body.appendChild(container)
  81. const clientComp = compileVaporComponent(code, data, components)
  82. const app = createVaporSSRApp(clientComp)
  83. app.mount(container)
  84. return {
  85. block: (app._instance! as VaporComponentInstance).block,
  86. container,
  87. }
  88. }
  89. async function testHydration(
  90. code: string,
  91. components: Record<string, string | { code: string; vapor: boolean }> = {},
  92. data: any = ref('foo'),
  93. { isVaporApp = true, interop = false } = {},
  94. ) {
  95. const ssrComponents: any = {}
  96. const clientComponents: any = {}
  97. for (const key in components) {
  98. const comp = components[key]
  99. const code = isString(comp) ? comp : comp.code
  100. const isVaporComp = isString(comp) || !!comp.vapor
  101. clientComponents[key] = compile(code, data, clientComponents, {
  102. vapor: isVaporComp,
  103. ssr: false,
  104. })
  105. ssrComponents[key] = compile(code, data, ssrComponents, {
  106. vapor: isVaporComp,
  107. ssr: true,
  108. })
  109. }
  110. const serverComp = compile(code, data, ssrComponents, {
  111. vapor: isVaporApp,
  112. ssr: true,
  113. })
  114. const html = await VueServerRenderer.renderToString(
  115. runtimeDom.createSSRApp(serverComp),
  116. )
  117. const container = document.createElement('div')
  118. document.body.appendChild(container)
  119. container.innerHTML = html
  120. const clientComp = compile(code, data, clientComponents, {
  121. vapor: isVaporApp,
  122. ssr: false,
  123. })
  124. let app
  125. if (isVaporApp) {
  126. app = createVaporSSRApp(clientComp)
  127. } else {
  128. app = runtimeDom.createSSRApp(clientComp)
  129. }
  130. if (interop) {
  131. app.use(runtimeVapor.vaporInteropPlugin)
  132. }
  133. app.mount(container)
  134. return { data, container }
  135. }
  136. const triggerEvent = (type: string, el: Element) => {
  137. const event = new Event(type, { bubbles: true })
  138. el.dispatchEvent(event)
  139. }
  140. delegateEvents('click')
  141. beforeEach(() => {
  142. document.body.innerHTML = ''
  143. })
  144. describe('Vapor Mode hydration', () => {
  145. describe('text', () => {
  146. test('root text', async () => {
  147. const { data, container } = await testHydration(`
  148. <template>{{ data }}</template>
  149. `)
  150. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"foo"`)
  151. data.value = 'bar'
  152. await nextTick()
  153. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"bar"`)
  154. })
  155. test('consecutive text nodes', async () => {
  156. const { data, container } = await testHydration(`
  157. <template>{{ data }}{{ data }}</template>
  158. `)
  159. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"foofoo"`)
  160. data.value = 'bar'
  161. await nextTick()
  162. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"barbar"`)
  163. })
  164. test('consecutive text nodes with insertion anchor', async () => {
  165. const { data, container } = await testHydration(`
  166. <template><span/>{{ data }}{{ data }}<span/></template>
  167. `)
  168. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  169. `
  170. "
  171. <!--[--><span></span>foofoo<span></span><!--]-->
  172. "
  173. `,
  174. )
  175. data.value = 'bar'
  176. await nextTick()
  177. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  178. `
  179. "
  180. <!--[--><span></span>barbar<span></span><!--]-->
  181. "
  182. `,
  183. )
  184. })
  185. test('mixed text nodes', async () => {
  186. const { data, container } = await testHydration(`
  187. <template>{{ data }}A{{ data }}B{{ data }}</template>
  188. `)
  189. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  190. `"fooAfooBfoo"`,
  191. )
  192. data.value = 'bar'
  193. await nextTick()
  194. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  195. `"barAbarBbar"`,
  196. )
  197. })
  198. test('mixed text nodes with insertion anchor', async () => {
  199. const { data, container } = await testHydration(`
  200. <template><span/>{{ data }}A{{ data }}B{{ data }}<span/></template>
  201. `)
  202. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  203. `
  204. "
  205. <!--[--><span></span>fooAfooBfoo<span></span><!--]-->
  206. "
  207. `,
  208. )
  209. data.value = 'bar'
  210. await nextTick()
  211. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  212. `
  213. "
  214. <!--[--><span></span>barAbarBbar<span></span><!--]-->
  215. "
  216. `,
  217. )
  218. })
  219. test('empty text node', async () => {
  220. const data = reactive({ txt: '' })
  221. const { container } = await testHydration(
  222. `<template><div>{{ data.txt }}</div></template>`,
  223. undefined,
  224. data,
  225. )
  226. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  227. `"<div></div>"`,
  228. )
  229. data.txt = 'foo'
  230. await nextTick()
  231. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  232. `"<div>foo</div>"`,
  233. )
  234. })
  235. test('empty text node in slot', async () => {
  236. const data = reactive({ txt: '' })
  237. const { container } = await testHydration(
  238. `<template><components.Child>{{data.txt}}</components.Child></template>`,
  239. {
  240. Child: `<template><slot/></template>`,
  241. },
  242. data,
  243. )
  244. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  245. `
  246. "
  247. <!--[--><!--]-->
  248. "
  249. `,
  250. )
  251. data.txt = 'foo'
  252. await nextTick()
  253. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  254. `
  255. "
  256. <!--[-->foo<!--]-->
  257. "
  258. `,
  259. )
  260. })
  261. })
  262. describe('element', () => {
  263. test('root comment', async () => {
  264. const { container } = await testHydration(`
  265. <template><!----></template>
  266. `)
  267. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"<!---->"`)
  268. expect(`mismatch in <div>`).not.toHaveBeenWarned()
  269. })
  270. test('root with mixed element and text', async () => {
  271. const { container, data } = await testHydration(`
  272. <template> A<span>{{ data }}</span>{{ data }}</template>
  273. `)
  274. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  275. `
  276. "
  277. <!--[--> A<span>foo</span>foo<!--]-->
  278. "
  279. `,
  280. )
  281. data.value = 'bar'
  282. await nextTick()
  283. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  284. `
  285. "
  286. <!--[--> A<span>bar</span>bar<!--]-->
  287. "
  288. `,
  289. )
  290. })
  291. test('empty element', async () => {
  292. const { container } = await testHydration(`
  293. <template><div/></template>
  294. `)
  295. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  296. `"<div></div>"`,
  297. )
  298. expect(`mismatch in <div>`).not.toHaveBeenWarned()
  299. })
  300. test('element with binding and text children', async () => {
  301. const { container, data } = await testHydration(`
  302. <template><div :class="data">{{ data }}</div></template>
  303. `)
  304. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  305. `"<div class="foo">foo</div>"`,
  306. )
  307. data.value = 'bar'
  308. await nextTick()
  309. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  310. `"<div class="bar">bar</div>"`,
  311. )
  312. })
  313. test('element with elements children', async () => {
  314. const { container } = await testHydration(`
  315. <template>
  316. <div>
  317. <span>{{ data }}</span>
  318. <span :class="data" @click="data = 'bar'"/>
  319. </div>
  320. </template>
  321. `)
  322. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  323. `"<div><span>foo</span><span class="foo"></span></div>"`,
  324. )
  325. // event handler
  326. triggerEvent('click', container.querySelector('.foo')!)
  327. await nextTick()
  328. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  329. `"<div><span>bar</span><span class="bar"></span></div>"`,
  330. )
  331. })
  332. test('element with ref', async () => {
  333. const { data, container } = await testHydration(
  334. `<template>
  335. <div ref="data">hi</div>
  336. </template>
  337. `,
  338. {},
  339. ref(null),
  340. )
  341. expect(data.value).toBe(container.firstChild)
  342. })
  343. })
  344. describe('component', () => {
  345. test('basic component', async () => {
  346. const { container, data } = await testHydration(
  347. `
  348. <template><div><span></span><components.Child/></div></template>
  349. `,
  350. { Child: `<template>{{ data }}</template>` },
  351. )
  352. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  353. `"<div><span></span>foo</div>"`,
  354. )
  355. data.value = 'bar'
  356. await nextTick()
  357. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  358. `"<div><span></span>bar</div>"`,
  359. )
  360. })
  361. test('fragment component', async () => {
  362. const { container, data } = await testHydration(
  363. `
  364. <template><div><span></span><components.Child/></div></template>
  365. `,
  366. { Child: `<template><div>{{ data }}</div>-{{ data }}-</template>` },
  367. )
  368. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  369. `
  370. "<div><span></span>
  371. <!--[--><div>foo</div>-foo-<!--]-->
  372. </div>"
  373. `,
  374. )
  375. data.value = 'bar'
  376. await nextTick()
  377. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  378. `
  379. "<div><span></span>
  380. <!--[--><div>bar</div>-bar-<!--]-->
  381. </div>"
  382. `,
  383. )
  384. })
  385. test('fragment component with prepend', async () => {
  386. const { container, data } = await testHydration(
  387. `
  388. <template><div><components.Child/><span></span></div></template>
  389. `,
  390. { Child: `<template><div>{{ data }}</div>-{{ data }}-</template>` },
  391. )
  392. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  393. `
  394. "<div>
  395. <!--[--><div>foo</div>-foo-<!--]-->
  396. <span></span></div>"
  397. `,
  398. )
  399. data.value = 'bar'
  400. await nextTick()
  401. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  402. `
  403. "<div>
  404. <!--[--><div>bar</div>-bar-<!--]-->
  405. <span></span></div>"
  406. `,
  407. )
  408. })
  409. test('nested fragment components', async () => {
  410. const { container, data } = await testHydration(
  411. `
  412. <template><div><components.Parent/><span></span></div></template>
  413. `,
  414. {
  415. Parent: `<template><div/><components.Child/><div/></template>`,
  416. Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
  417. },
  418. )
  419. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  420. `
  421. "<div>
  422. <!--[--><div></div>
  423. <!--[--><div>foo</div>-foo-<!--]-->
  424. <div></div><!--]-->
  425. <span></span></div>"
  426. `,
  427. )
  428. data.value = 'bar'
  429. await nextTick()
  430. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  431. `
  432. "<div>
  433. <!--[--><div></div>
  434. <!--[--><div>bar</div>-bar-<!--]-->
  435. <div></div><!--]-->
  436. <span></span></div>"
  437. `,
  438. )
  439. })
  440. test('component with insertion anchor', async () => {
  441. const { container, data } = await testHydration(
  442. `<template>
  443. <div>
  444. <span/>
  445. <components.Child/>
  446. <span/>
  447. </div>
  448. </template>
  449. `,
  450. {
  451. Child: `<template>{{ data }}</template>`,
  452. },
  453. )
  454. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  455. `"<div><span></span>foo<span></span></div>"`,
  456. )
  457. data.value = 'bar'
  458. await nextTick()
  459. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  460. `"<div><span></span>bar<span></span></div>"`,
  461. )
  462. })
  463. test('nested components with insertion anchor', async () => {
  464. const { container, data } = await testHydration(
  465. `
  466. <template><components.Parent/></template>
  467. `,
  468. {
  469. Parent: `<template><div><span/><components.Child/><span/></div></template>`,
  470. Child: `<template><div>{{ data }}</div></template>`,
  471. },
  472. )
  473. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  474. `"<div><span></span><div>foo</div><span></span></div>"`,
  475. )
  476. data.value = 'bar'
  477. await nextTick()
  478. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  479. `"<div><span></span><div>bar</div><span></span></div>"`,
  480. )
  481. })
  482. test('nested components with multi level anchor insertion', async () => {
  483. const { container, data } = await testHydration(
  484. `
  485. <template><div><span></span><components.Parent/><span></span></div></template>
  486. `,
  487. {
  488. Parent: `<template><div><span/><components.Child/><span/></div></template>`,
  489. Child: `<template><div>{{ data }}</div></template>`,
  490. },
  491. )
  492. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  493. `"<div><span></span><div><span></span><div>foo</div><span></span></div><span></span></div>"`,
  494. )
  495. data.value = 'bar'
  496. await nextTick()
  497. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  498. `"<div><span></span><div><span></span><div>bar</div><span></span></div><span></span></div>"`,
  499. )
  500. })
  501. test('consecutive components with insertion parent', async () => {
  502. const data = reactive({ foo: 'foo', bar: 'bar' })
  503. const { container } = await testHydration(
  504. `<template>
  505. <div>
  506. <components.Child1/>
  507. <components.Child2/>
  508. </div>
  509. </template>
  510. `,
  511. {
  512. Child1: `<template><span>{{ data.foo }}</span></template>`,
  513. Child2: `<template><span>{{ data.bar }}</span></template>`,
  514. },
  515. data,
  516. )
  517. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  518. `"<div><span>foo</span><span>bar</span></div>"`,
  519. )
  520. data.foo = 'foo1'
  521. data.bar = 'bar1'
  522. await nextTick()
  523. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  524. `"<div><span>foo1</span><span>bar1</span></div>"`,
  525. )
  526. })
  527. test('nested consecutive components with insertion anchor', async () => {
  528. const { container, data } = await testHydration(
  529. `
  530. <template><components.Parent/></template>
  531. `,
  532. {
  533. Parent: `<template><div><span/><components.Child/><components.Child/><span/></div></template>`,
  534. Child: `<template><div>{{ data }}</div></template>`,
  535. },
  536. )
  537. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  538. `"<div><span></span><div>foo</div><div>foo</div><span></span></div>"`,
  539. )
  540. data.value = 'bar'
  541. await nextTick()
  542. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  543. `"<div><span></span><div>bar</div><div>bar</div><span></span></div>"`,
  544. )
  545. })
  546. test('nested consecutive components with multi level anchor insertion', async () => {
  547. const { container, data } = await testHydration(
  548. `
  549. <template><div><span></span><components.Parent/><span></span></div></template>
  550. `,
  551. {
  552. Parent: `<template><div><span/><components.Child/><components.Child/><span/></div></template>`,
  553. Child: `<template><div>{{ data }}</div></template>`,
  554. },
  555. )
  556. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  557. `"<div><span></span><div><span></span><div>foo</div><div>foo</div><span></span></div><span></span></div>"`,
  558. )
  559. data.value = 'bar'
  560. await nextTick()
  561. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  562. `"<div><span></span><div><span></span><div>bar</div><div>bar</div><span></span></div><span></span></div>"`,
  563. )
  564. })
  565. test('mixed component and element with insertion anchor', async () => {
  566. const { container, data } = await testHydration(
  567. `<template>
  568. <div>
  569. <span/>
  570. <components.Child/>
  571. <span/>
  572. <components.Child/>
  573. <span/>
  574. </div>
  575. </template>
  576. `,
  577. {
  578. Child: `<template>{{ data }}</template>`,
  579. },
  580. )
  581. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  582. `"<div><span></span>foo<span></span>foo<span></span></div>"`,
  583. )
  584. data.value = 'bar'
  585. await nextTick()
  586. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  587. `"<div><span></span>bar<span></span>bar<span></span></div>"`,
  588. )
  589. })
  590. test('fragment component with insertion anchor', async () => {
  591. const { container, data } = await testHydration(
  592. `<template>
  593. <div>
  594. <span/>
  595. <components.Child/>
  596. <span/>
  597. </div>
  598. </template>
  599. `,
  600. {
  601. Child: `<template><div>{{ data }}</div>-{{ data }}</template>`,
  602. },
  603. )
  604. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  605. `
  606. "<div><span></span>
  607. <!--[--><div>foo</div>-foo<!--]-->
  608. <span></span></div>"
  609. `,
  610. )
  611. data.value = 'bar'
  612. await nextTick()
  613. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  614. `
  615. "<div><span></span>
  616. <!--[--><div>bar</div>-bar<!--]-->
  617. <span></span></div>"
  618. `,
  619. )
  620. })
  621. test('nested fragment component with insertion anchor', async () => {
  622. const { container, data } = await testHydration(
  623. `
  624. <template><components.Parent/></template>
  625. `,
  626. {
  627. Parent: `<template><div><span/><components.Child/><span/></div></template>`,
  628. Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
  629. },
  630. )
  631. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  632. `
  633. "<div><span></span>
  634. <!--[--><div>foo</div>-foo-<!--]-->
  635. <span></span></div>"
  636. `,
  637. )
  638. data.value = 'bar'
  639. await nextTick()
  640. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  641. `
  642. "<div><span></span>
  643. <!--[--><div>bar</div>-bar-<!--]-->
  644. <span></span></div>"
  645. `,
  646. )
  647. })
  648. test('nested fragment component with multi level anchor insertion', async () => {
  649. const { container, data } = await testHydration(
  650. `
  651. <template><div><span/><components.Parent/><span/></div></template>
  652. `,
  653. {
  654. Parent: `<template><div><span/><components.Child/><span/></div></template>`,
  655. Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
  656. },
  657. )
  658. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  659. `
  660. "<div><span></span><div><span></span>
  661. <!--[--><div>foo</div>-foo-<!--]-->
  662. <span></span></div><span></span></div>"
  663. `,
  664. )
  665. data.value = 'bar'
  666. await nextTick()
  667. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  668. `
  669. "<div><span></span><div><span></span>
  670. <!--[--><div>bar</div>-bar-<!--]-->
  671. <span></span></div><span></span></div>"
  672. `,
  673. )
  674. })
  675. test('consecutive fragment components with insertion anchor', async () => {
  676. const { container, data } = await testHydration(
  677. `<template>
  678. <div>
  679. <span/>
  680. <components.Child/>
  681. <components.Child/>
  682. <span/>
  683. </div>
  684. </template>
  685. `,
  686. {
  687. Child: `<template><div>{{ data }}</div>-{{ data }}</template>`,
  688. },
  689. )
  690. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  691. `
  692. "<div><span></span>
  693. <!--[--><div>foo</div>-foo<!--]-->
  694. <!--[--><div>foo</div>-foo<!--]-->
  695. <span></span></div>"
  696. `,
  697. )
  698. data.value = 'bar'
  699. await nextTick()
  700. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  701. `
  702. "<div><span></span>
  703. <!--[--><div>bar</div>-bar<!--]-->
  704. <!--[--><div>bar</div>-bar<!--]-->
  705. <span></span></div>"
  706. `,
  707. )
  708. })
  709. test('nested consecutive fragment components with insertion anchor', async () => {
  710. const { container, data } = await testHydration(
  711. `
  712. <template><components.Parent/></template>
  713. `,
  714. {
  715. Parent: `<template><div><span/><components.Child/><components.Child/><span/></div></template>`,
  716. Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
  717. },
  718. )
  719. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  720. `
  721. "<div><span></span>
  722. <!--[--><div>foo</div>-foo-<!--]-->
  723. <!--[--><div>foo</div>-foo-<!--]-->
  724. <span></span></div>"
  725. `,
  726. )
  727. data.value = 'bar'
  728. await nextTick()
  729. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  730. `
  731. "<div><span></span>
  732. <!--[--><div>bar</div>-bar-<!--]-->
  733. <!--[--><div>bar</div>-bar-<!--]-->
  734. <span></span></div>"
  735. `,
  736. )
  737. })
  738. test('nested consecutive fragment components with multi level anchor insertion', async () => {
  739. const { container, data } = await testHydration(
  740. `
  741. <template><div><span></span><components.Parent/><span></span></div></template>
  742. `,
  743. {
  744. Parent: `<template><div><span/><components.Child/><components.Child/><span/></div></template>`,
  745. Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
  746. },
  747. )
  748. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  749. `
  750. "<div><span></span><div><span></span>
  751. <!--[--><div>foo</div>-foo-<!--]-->
  752. <!--[--><div>foo</div>-foo-<!--]-->
  753. <span></span></div><span></span></div>"
  754. `,
  755. )
  756. data.value = 'bar'
  757. await nextTick()
  758. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  759. `
  760. "<div><span></span><div><span></span>
  761. <!--[--><div>bar</div>-bar-<!--]-->
  762. <!--[--><div>bar</div>-bar-<!--]-->
  763. <span></span></div><span></span></div>"
  764. `,
  765. )
  766. })
  767. test('nested consecutive fragment components with root level anchor insertion', async () => {
  768. const { container, data } = await testHydration(
  769. `
  770. <template><div><span></span><components.Parent/><span></span></div></template>
  771. `,
  772. {
  773. Parent: `<template><components.Child/><components.Child/></template>`,
  774. Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
  775. },
  776. )
  777. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  778. `
  779. "<div><span></span>
  780. <!--[-->
  781. <!--[--><div>foo</div>-foo-<!--]-->
  782. <!--[--><div>foo</div>-foo-<!--]-->
  783. <!--]-->
  784. <span></span></div>"
  785. `,
  786. )
  787. data.value = 'bar'
  788. await nextTick()
  789. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  790. `
  791. "<div><span></span>
  792. <!--[-->
  793. <!--[--><div>bar</div>-bar-<!--]-->
  794. <!--[--><div>bar</div>-bar-<!--]-->
  795. <!--]-->
  796. <span></span></div>"
  797. `,
  798. )
  799. })
  800. test('mixed fragment component and element with insertion anchor', async () => {
  801. const { container, data } = await testHydration(
  802. `<template>
  803. <div>
  804. <span/>
  805. <components.Child/>
  806. <span/>
  807. <components.Child/>
  808. <span/>
  809. </div>
  810. </template>
  811. `,
  812. {
  813. Child: `<template><div>{{ data }}</div>-{{ data }}</template>`,
  814. },
  815. )
  816. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  817. `
  818. "<div><span></span>
  819. <!--[--><div>foo</div>-foo<!--]-->
  820. <span></span>
  821. <!--[--><div>foo</div>-foo<!--]-->
  822. <span></span></div>"
  823. `,
  824. )
  825. data.value = 'bar'
  826. await nextTick()
  827. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  828. `
  829. "<div><span></span>
  830. <!--[--><div>bar</div>-bar<!--]-->
  831. <span></span>
  832. <!--[--><div>bar</div>-bar<!--]-->
  833. <span></span></div>"
  834. `,
  835. )
  836. })
  837. test('mixed fragment component and text with insertion anchor', async () => {
  838. const { container, data } = await testHydration(
  839. `<template>
  840. <div>
  841. <span/>
  842. <components.Child/>
  843. {{ data }}
  844. <components.Child/>
  845. <span/>
  846. </div>
  847. </template>
  848. `,
  849. {
  850. Child: `<template><div>{{ data }}</div>-{{ data }}</template>`,
  851. },
  852. )
  853. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  854. `
  855. "<div><span></span>
  856. <!--[--><div>foo</div>-foo<!--]-->
  857. foo
  858. <!--[--><div>foo</div>-foo<!--]-->
  859. <span></span></div>"
  860. `,
  861. )
  862. data.value = 'bar'
  863. await nextTick()
  864. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  865. `
  866. "<div><span></span>
  867. <!--[--><div>bar</div>-bar<!--]-->
  868. bar
  869. <!--[--><div>bar</div>-bar<!--]-->
  870. <span></span></div>"
  871. `,
  872. )
  873. })
  874. })
  875. describe('dynamic component', () => {
  876. test('basic dynamic component', async () => {
  877. const { container, data } = await testHydration(
  878. `<template>
  879. <component :is="components[data]"/>
  880. </template>`,
  881. {
  882. foo: `<template><div>foo</div></template>`,
  883. bar: `<template><div>bar</div></template>`,
  884. },
  885. ref('foo'),
  886. )
  887. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  888. `"<div>foo</div><!--dynamic-component-->"`,
  889. )
  890. data.value = 'bar'
  891. await nextTick()
  892. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  893. `"<div>bar</div><!--dynamic-component-->"`,
  894. )
  895. })
  896. test('dynamic component with insertion anchor', async () => {
  897. const { container, data } = await testHydration(
  898. `<template>
  899. <div>
  900. <span/>
  901. <component :is="components[data]"/>
  902. <span/>
  903. </div>
  904. </template>`,
  905. {
  906. foo: `<template><div>foo</div></template>`,
  907. bar: `<template><div>bar</div></template>`,
  908. },
  909. ref('foo'),
  910. )
  911. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  912. `"<div><span></span><div>foo</div><!--dynamic-component--><span></span></div>"`,
  913. )
  914. data.value = 'bar'
  915. await nextTick()
  916. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  917. `"<div><span></span><div>bar</div><!--dynamic-component--><span></span></div>"`,
  918. )
  919. })
  920. test('consecutive dynamic components with insertion anchor', async () => {
  921. const { container, data } = await testHydration(
  922. `<template>
  923. <div>
  924. <span/>
  925. <component :is="components[data]"/>
  926. <component :is="components[data]"/>
  927. <span/>
  928. </div>
  929. </template>`,
  930. {
  931. foo: `<template><div>foo</div></template>`,
  932. bar: `<template><div>bar</div></template>`,
  933. },
  934. ref('foo'),
  935. )
  936. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  937. `"<div><span></span><div>foo</div><!--dynamic-component--><div>foo</div><!--dynamic-component--><span></span></div>"`,
  938. )
  939. data.value = 'bar'
  940. await nextTick()
  941. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  942. `"<div><span></span><div>bar</div><!--dynamic-component--><div>bar</div><!--dynamic-component--><span></span></div>"`,
  943. )
  944. })
  945. test('dynamic component fallback', async () => {
  946. const { container, data } = await testHydration(
  947. `<template>
  948. <component :is="'button'">
  949. <span>{{ data }}</span>
  950. </component>
  951. </template>`,
  952. {},
  953. ref('foo'),
  954. )
  955. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  956. `"<button><span>foo</span></button><!--dynamic-component-->"`,
  957. )
  958. data.value = 'bar'
  959. await nextTick()
  960. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  961. `"<button><span>bar</span></button><!--dynamic-component-->"`,
  962. )
  963. })
  964. test('in ssr slot vnode fallback', async () => {
  965. const { container, data } = await testHydration(
  966. `<template>
  967. <components.Child>
  968. <span>{{ data }}</span>
  969. </components.Child>
  970. </template>`,
  971. {
  972. Child: `
  973. <template>
  974. <component :is="'div'">
  975. <slot />
  976. </component>
  977. </template>`,
  978. },
  979. ref('foo'),
  980. )
  981. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  982. `
  983. "<div>
  984. <!--[--><span>foo</span><!--]-->
  985. </div><!--dynamic-component-->"
  986. `,
  987. )
  988. data.value = 'bar'
  989. await nextTick()
  990. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  991. `
  992. "<div>
  993. <!--[--><span>bar</span><!--]-->
  994. </div><!--dynamic-component-->"
  995. `,
  996. )
  997. })
  998. test('dynamic component fallback with dynamic slots', async () => {
  999. const data = ref({
  1000. name: 'default',
  1001. msg: 'foo',
  1002. })
  1003. const { container } = await testHydration(
  1004. `<template>
  1005. <component :is="'div'">
  1006. <template v-slot:[data.name]>
  1007. <span>{{ data.msg }}</span>
  1008. </template>
  1009. </component>
  1010. </template>`,
  1011. {},
  1012. data,
  1013. )
  1014. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1015. `"<div><span>foo</span><!----></div><!--dynamic-component-->"`,
  1016. )
  1017. data.value.msg = 'bar'
  1018. await nextTick()
  1019. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1020. `"<div><span>bar</span><!----></div><!--dynamic-component-->"`,
  1021. )
  1022. })
  1023. })
  1024. describe('if', () => {
  1025. test('basic toggle - true -> false', async () => {
  1026. const data = ref(true)
  1027. const { container } = await testHydration(
  1028. `<template>
  1029. <div v-if="data">foo</div>
  1030. </template>`,
  1031. undefined,
  1032. data,
  1033. )
  1034. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1035. `"<div>foo</div><!--if-->"`,
  1036. )
  1037. data.value = false
  1038. await nextTick()
  1039. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1040. `"<!--if-->"`,
  1041. )
  1042. })
  1043. test('basic toggle - false -> true', async () => {
  1044. const data = ref(false)
  1045. const { container } = await testHydration(
  1046. `<template>
  1047. <div v-if="data">foo</div>
  1048. </template>`,
  1049. undefined,
  1050. data,
  1051. )
  1052. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1053. `"<!--if-->"`,
  1054. )
  1055. data.value = true
  1056. await nextTick()
  1057. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1058. `"<div>foo</div><!--if-->"`,
  1059. )
  1060. })
  1061. test('v-if on insertion parent', async () => {
  1062. const data = ref(true)
  1063. const { container } = await testHydration(
  1064. `<template>
  1065. <div v-if="data">
  1066. <components.Child/>
  1067. </div>
  1068. </template>`,
  1069. { Child: `<template>foo</template>` },
  1070. data,
  1071. )
  1072. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1073. `"<div>foo</div><!--if-->"`,
  1074. )
  1075. data.value = false
  1076. await nextTick()
  1077. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1078. `"<!--if-->"`,
  1079. )
  1080. data.value = true
  1081. await nextTick()
  1082. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1083. `"<div>foo</div><!--if-->"`,
  1084. )
  1085. })
  1086. test('v-if/else-if/else chain - switch branches', async () => {
  1087. const data = ref('a')
  1088. const { container } = await testHydration(
  1089. `<template>
  1090. <div v-if="data === 'a'">foo</div>
  1091. <div v-else-if="data === 'b'">bar</div>
  1092. <div v-else>baz</div>
  1093. </template>`,
  1094. undefined,
  1095. data,
  1096. )
  1097. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1098. `"<div>foo</div><!--if-->"`,
  1099. )
  1100. data.value = 'b'
  1101. await nextTick()
  1102. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1103. `"<div>bar</div><!--if--><!--if-->"`,
  1104. )
  1105. data.value = 'c'
  1106. await nextTick()
  1107. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1108. `"<div>baz</div><!--if--><!--if-->"`,
  1109. )
  1110. data.value = 'a'
  1111. await nextTick()
  1112. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1113. `"<div>foo</div><!--if-->"`,
  1114. )
  1115. })
  1116. test('v-if/else with sibling components and elements', async () => {
  1117. const data = ref('a')
  1118. const { container } = await testHydration(
  1119. `<script setup>
  1120. const msg = _data
  1121. const { Comp } = _components
  1122. </script>
  1123. <template>
  1124. <div>
  1125. <Comp/>
  1126. <div>11</div>
  1127. <div v-if="msg === 'a'">foo</div>
  1128. <div v-else>baz</div>
  1129. <div>11</div>
  1130. <Comp/>
  1131. </div>
  1132. </template>`,
  1133. {
  1134. Comp: `<template><span>comp</span></template>`,
  1135. },
  1136. data,
  1137. )
  1138. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1139. `"<div><span>comp</span><div>11</div><div>foo</div><!--if--><div>11</div><span>comp</span></div>"`,
  1140. )
  1141. data.value = 'b'
  1142. await nextTick()
  1143. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1144. `"<div><span>comp</span><div>11</div><div>baz</div><!--if--><div>11</div><span>comp</span></div>"`,
  1145. )
  1146. })
  1147. test('nested if', async () => {
  1148. const data = reactive({ outer: true, inner: true })
  1149. const { container } = await testHydration(
  1150. `<template>
  1151. <div v-if="data.outer">
  1152. <span>outer</span>
  1153. <div v-if="data.inner">inner</div>
  1154. </div>
  1155. </template>`,
  1156. undefined,
  1157. data,
  1158. )
  1159. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1160. `"<div><span>outer</span><div>inner</div><!--if--></div><!--if-->"`,
  1161. )
  1162. data.inner = false
  1163. await nextTick()
  1164. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1165. `"<div><span>outer</span><!--if--></div><!--if-->"`,
  1166. )
  1167. data.outer = false
  1168. await nextTick()
  1169. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1170. `"<!--if-->"`,
  1171. )
  1172. })
  1173. test('on component', async () => {
  1174. const data = ref(true)
  1175. const { container } = await testHydration(
  1176. `<template>
  1177. <components.Child v-if="data"/>
  1178. </template>`,
  1179. { Child: `<template>foo</template>` },
  1180. data,
  1181. )
  1182. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1183. `"foo<!--if-->"`,
  1184. )
  1185. data.value = false
  1186. await nextTick()
  1187. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1188. `"<!--if-->"`,
  1189. )
  1190. })
  1191. test('consecutive if node', async () => {
  1192. const data = ref(true)
  1193. const { container } = await testHydration(
  1194. `<template>
  1195. <components.Child v-if="data"/>
  1196. </template>`,
  1197. { Child: `<template><div v-if="data">foo</div></template>` },
  1198. data,
  1199. )
  1200. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1201. `"<div>foo</div><!--if--><!--if-->"`,
  1202. )
  1203. data.value = false
  1204. await nextTick()
  1205. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1206. `"<!--if-->"`,
  1207. )
  1208. data.value = true
  1209. await nextTick()
  1210. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1211. `"<div>foo</div><!--if--><!--if-->"`,
  1212. )
  1213. })
  1214. test('mixed prepend and insertion anchor', async () => {
  1215. const data = reactive({
  1216. show: true,
  1217. foo: 'foo',
  1218. bar: 'bar',
  1219. qux: 'qux',
  1220. })
  1221. const { container } = await testHydration(
  1222. `<template>
  1223. <components.Child/>
  1224. </template>`,
  1225. {
  1226. Child: `<template>
  1227. <span v-if="data.show">
  1228. <span v-if="data.show">{{data.foo}}</span>
  1229. <span v-if="data.show">{{data.bar}}</span>
  1230. <span>baz</span>
  1231. <span v-if="data.show">{{data.qux}}</span>
  1232. <span>quux</span>
  1233. </span>
  1234. </template>`,
  1235. },
  1236. data,
  1237. )
  1238. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1239. `"<span><span>foo</span><!--if--><span>bar</span><!--if--><span>baz</span><span>qux</span><!--if--><span>quux</span></span><!--if-->"`,
  1240. )
  1241. data.qux = 'qux1'
  1242. await nextTick()
  1243. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1244. `"<span><span>foo</span><!--if--><span>bar</span><!--if--><span>baz</span><span>qux1</span><!--if--><span>quux</span></span><!--if-->"`,
  1245. )
  1246. data.foo = 'foo1'
  1247. await nextTick()
  1248. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1249. `"<span><span>foo1</span><!--if--><span>bar</span><!--if--><span>baz</span><span>qux1</span><!--if--><span>quux</span></span><!--if-->"`,
  1250. )
  1251. })
  1252. test('v-if/else-if/else chain on component - switch branches', async () => {
  1253. const data = ref('a')
  1254. const { container } = await testHydration(
  1255. `<template>
  1256. <components.Child1 v-if="data === 'a'"/>
  1257. <components.Child2 v-else-if="data === 'b'"/>
  1258. <components.Child3 v-else/>
  1259. </template>`,
  1260. {
  1261. Child1: `<template><span>{{data}} child1</span></template>`,
  1262. Child2: `<template><span>{{data}} child2</span></template>`,
  1263. Child3: `<template><span>{{data}} child3</span></template>`,
  1264. },
  1265. data,
  1266. )
  1267. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1268. `"<span>a child1</span><!--if-->"`,
  1269. )
  1270. data.value = 'b'
  1271. await nextTick()
  1272. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1273. `"<span>b child2</span><!--if--><!--if-->"`,
  1274. )
  1275. data.value = 'c'
  1276. await nextTick()
  1277. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1278. `"<span>c child3</span><!--if--><!--if-->"`,
  1279. )
  1280. data.value = 'a'
  1281. await nextTick()
  1282. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1283. `"<span>a child1</span><!--if-->"`,
  1284. )
  1285. })
  1286. test('on component with insertion anchor', async () => {
  1287. const data = ref(true)
  1288. const { container } = await testHydration(
  1289. `<template>
  1290. <div>
  1291. <span/>
  1292. <components.Child v-if="data"/>
  1293. <span/>
  1294. </div>
  1295. </template>`,
  1296. { Child: `<template>foo</template>` },
  1297. data,
  1298. )
  1299. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1300. `"<div><span></span>foo<!--if--><span></span></div>"`,
  1301. )
  1302. data.value = false
  1303. await nextTick()
  1304. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1305. `"<div><span></span><!--if--><span></span></div>"`,
  1306. )
  1307. })
  1308. test('consecutive component with insertion parent', async () => {
  1309. const data = reactive({
  1310. show: true,
  1311. foo: 'foo',
  1312. bar: 'bar',
  1313. })
  1314. const { container } = await testHydration(
  1315. `<template>
  1316. <div v-if="data.show">
  1317. <components.Child/>
  1318. <components.Child2/>
  1319. </div>
  1320. </template>`,
  1321. {
  1322. Child: `<template><span>{{data.foo}}</span></template>`,
  1323. Child2: `<template><span>{{data.bar}}</span></template>`,
  1324. },
  1325. data,
  1326. )
  1327. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1328. `"<div><span>foo</span><span>bar</span></div><!--if-->"`,
  1329. )
  1330. data.show = false
  1331. await nextTick()
  1332. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1333. `"<!--if-->"`,
  1334. )
  1335. data.show = true
  1336. await nextTick()
  1337. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1338. `"<div><span>foo</span><span>bar</span></div><!--if-->"`,
  1339. )
  1340. data.foo = 'foo1'
  1341. data.bar = 'bar1'
  1342. await nextTick()
  1343. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1344. `"<div><span>foo1</span><span>bar1</span></div><!--if-->"`,
  1345. )
  1346. })
  1347. test('on fragment component', async () => {
  1348. const data = ref(true)
  1349. const { container } = await testHydration(
  1350. `<template>
  1351. <div>
  1352. <components.Child v-if="data"/>
  1353. </div>
  1354. </template>`,
  1355. {
  1356. Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
  1357. },
  1358. data,
  1359. )
  1360. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1361. `
  1362. "<div>
  1363. <!--[--><div>true</div>-true-<!--]-->
  1364. <!--if--></div>"
  1365. `,
  1366. )
  1367. data.value = false
  1368. await nextTick()
  1369. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1370. `
  1371. "<div>
  1372. <!--[--><!--]-->
  1373. <!--if--></div>"
  1374. `,
  1375. )
  1376. data.value = true
  1377. await nextTick()
  1378. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1379. `
  1380. "<div>
  1381. <!--[--><!--]-->
  1382. <div>true</div>-true-<!--if--></div>"
  1383. `,
  1384. )
  1385. })
  1386. test('on fragment component with insertion anchor', async () => {
  1387. const data = ref(true)
  1388. const { container } = await testHydration(
  1389. `<template>
  1390. <div>
  1391. <span/>
  1392. <components.Child v-if="data"/>
  1393. <span/>
  1394. </div>
  1395. </template>`,
  1396. {
  1397. Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
  1398. },
  1399. data,
  1400. )
  1401. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1402. `
  1403. "<div><span></span>
  1404. <!--[--><div>true</div>-true-<!--]-->
  1405. <!--if--><span></span></div>"
  1406. `,
  1407. )
  1408. data.value = false
  1409. await nextTick()
  1410. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1411. `
  1412. "<div><span></span>
  1413. <!--[--><!--]-->
  1414. <!--if--><span></span></div>"
  1415. `,
  1416. )
  1417. data.value = true
  1418. await nextTick()
  1419. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  1420. "<div><span></span>
  1421. <!--[--><!--]-->
  1422. <div>true</div>-true-<!--if--><span></span></div>"
  1423. `)
  1424. })
  1425. test('consecutive v-if on fragment component with insertion anchor', async () => {
  1426. const data = ref(true)
  1427. const { container } = await testHydration(
  1428. `<template>
  1429. <div>
  1430. <span/>
  1431. <components.Child v-if="data"/>
  1432. <components.Child v-if="data"/>
  1433. <span/>
  1434. </div>
  1435. </template>`,
  1436. {
  1437. Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
  1438. },
  1439. data,
  1440. )
  1441. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1442. `
  1443. "<div><span></span>
  1444. <!--[--><div>true</div>-true-<!--]-->
  1445. <!--if-->
  1446. <!--[--><div>true</div>-true-<!--]-->
  1447. <!--if--><span></span></div>"
  1448. `,
  1449. )
  1450. data.value = false
  1451. await nextTick()
  1452. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1453. `
  1454. "<div><span></span>
  1455. <!--[--><!--]-->
  1456. <!--if-->
  1457. <!--[--><!--]-->
  1458. <!--if--><span></span></div>"
  1459. `,
  1460. )
  1461. data.value = true
  1462. await nextTick()
  1463. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  1464. "<div><span></span>
  1465. <!--[--><!--]-->
  1466. <div>true</div>-true-<!--if-->
  1467. <!--[--><!--]-->
  1468. <div>true</div>-true-<!--if--><span></span></div>"
  1469. `)
  1470. })
  1471. test('on dynamic component with insertion anchor', async () => {
  1472. const data = ref(true)
  1473. const { container } = await testHydration(
  1474. `<template>
  1475. <div>
  1476. <span/>
  1477. <component :is="components.Child" v-if="data"/>
  1478. <span/>
  1479. </div>
  1480. </template>`,
  1481. { Child: `<template>foo</template>` },
  1482. data,
  1483. )
  1484. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1485. `"<div><span></span>foo<!--dynamic-component--><!--if--><span></span></div>"`,
  1486. )
  1487. data.value = false
  1488. await nextTick()
  1489. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1490. `"<div><span></span><!--if--><span></span></div>"`,
  1491. )
  1492. data.value = true
  1493. await nextTick()
  1494. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1495. `"<div><span></span>foo<!--dynamic-component--><!--if--><span></span></div>"`,
  1496. )
  1497. })
  1498. test('v-if with insertion parent + sibling component', async () => {
  1499. const data = ref(true)
  1500. const { container } = await testHydration(
  1501. `<template>
  1502. <div>
  1503. <span v-if="data">hello</span>
  1504. </div>
  1505. <components.Child/>
  1506. </template>`,
  1507. {
  1508. Child: `<template><div>child</div></template>`,
  1509. },
  1510. data,
  1511. )
  1512. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1513. `
  1514. "
  1515. <!--[--><div><span>hello</span><!--if--></div><div>child</div><!--]-->
  1516. "
  1517. `,
  1518. )
  1519. data.value = false
  1520. await nextTick()
  1521. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1522. `
  1523. "
  1524. <!--[--><div><!--if--></div><div>child</div><!--]-->
  1525. "
  1526. `,
  1527. )
  1528. data.value = true
  1529. await nextTick()
  1530. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1531. `
  1532. "
  1533. <!--[--><div><span>hello</span><!--if--></div><div>child</div><!--]-->
  1534. "
  1535. `,
  1536. )
  1537. })
  1538. test('v-if with static sibling + root sibling component', async () => {
  1539. const data = ref(true)
  1540. const { container } = await testHydration(
  1541. `<template>
  1542. <div>
  1543. <span v-if="data">hello</span>
  1544. <div>1</div>
  1545. </div>
  1546. <components.Child/>
  1547. </template>`,
  1548. {
  1549. Child: `<template><div>child</div></template>`,
  1550. },
  1551. data,
  1552. )
  1553. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1554. `
  1555. "
  1556. <!--[--><div><span>hello</span><!--if--><div>1</div></div><div>child</div><!--]-->
  1557. "
  1558. `,
  1559. )
  1560. data.value = false
  1561. await nextTick()
  1562. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1563. `
  1564. "
  1565. <!--[--><div><!--if--><div>1</div></div><div>child</div><!--]-->
  1566. "
  1567. `,
  1568. )
  1569. data.value = true
  1570. await nextTick()
  1571. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1572. `
  1573. "
  1574. <!--[--><div><span>hello</span><!--if--><div>1</div></div><div>child</div><!--]-->
  1575. "
  1576. `,
  1577. )
  1578. })
  1579. test('v-if + static sibling + root sibling component (flat)', async () => {
  1580. const data = ref(true)
  1581. const { container } = await testHydration(
  1582. `<template>
  1583. <span v-if="data">hello</span>
  1584. <span></span>
  1585. <components.Child/>
  1586. </template>`,
  1587. {
  1588. Child: `<template><div>child</div></template>`,
  1589. },
  1590. data,
  1591. )
  1592. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1593. `
  1594. "
  1595. <!--[--><span>hello</span><!--if--><span></span><div>child</div><!--]-->
  1596. "
  1597. `,
  1598. )
  1599. data.value = false
  1600. await nextTick()
  1601. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1602. `
  1603. "
  1604. <!--[--><!--if--><span></span><div>child</div><!--]-->
  1605. "
  1606. `,
  1607. )
  1608. data.value = true
  1609. await nextTick()
  1610. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1611. `
  1612. "
  1613. <!--[--><span>hello</span><!--if--><span></span><div>child</div><!--]-->
  1614. "
  1615. `,
  1616. )
  1617. })
  1618. })
  1619. describe('for', () => {
  1620. test('basic v-for', async () => {
  1621. const { container, data } = await testHydration(
  1622. `<template>
  1623. <span v-for="item in data" :key="item">{{ item }}</span>
  1624. </template>`,
  1625. undefined,
  1626. ref(['a', 'b', 'c']),
  1627. )
  1628. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1629. `
  1630. "
  1631. <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
  1632. "
  1633. `,
  1634. )
  1635. data.value.push('d')
  1636. await nextTick()
  1637. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1638. `
  1639. "
  1640. <!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
  1641. "
  1642. `,
  1643. )
  1644. })
  1645. test('empty v-for', async () => {
  1646. const { container, data } = await testHydration(
  1647. `<template>
  1648. <span v-for="item in data" :key="item">{{ item }}</span>
  1649. </template>`,
  1650. undefined,
  1651. ref([]),
  1652. )
  1653. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1654. `
  1655. "
  1656. <!--[--><!--]-->
  1657. "
  1658. `,
  1659. )
  1660. data.value.push('a')
  1661. await nextTick()
  1662. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1663. `
  1664. "
  1665. <!--[--><span>a</span><!--]-->
  1666. "
  1667. `,
  1668. )
  1669. })
  1670. test('v-for with insertion parent + sibling component', async () => {
  1671. const { container, data } = await testHydration(
  1672. `<template>
  1673. <div>
  1674. <span v-for="item in data" :key="item">{{ item }}</span>
  1675. </div>
  1676. <components.Child/>
  1677. </template>`,
  1678. {
  1679. Child: `<template><div>{{data.length}}</div></template>`,
  1680. },
  1681. ref(['a', 'b', 'c']),
  1682. )
  1683. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1684. `
  1685. "
  1686. <!--[--><div>
  1687. <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
  1688. </div><div>3</div><!--]-->
  1689. "
  1690. `,
  1691. )
  1692. data.value.push('d')
  1693. await nextTick()
  1694. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1695. `
  1696. "
  1697. <!--[--><div>
  1698. <!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
  1699. </div><div>4</div><!--]-->
  1700. "
  1701. `,
  1702. )
  1703. })
  1704. test('v-for with static sibling + root sibling component', async () => {
  1705. const { container, data } = await testHydration(
  1706. `<template>
  1707. <div>
  1708. <span v-for="item in data" :key="item">{{ item }}</span>
  1709. <div>1</div>
  1710. </div>
  1711. <components.Child/>
  1712. </template>`,
  1713. {
  1714. Child: `<template><div>{{data.length}}</div></template>`,
  1715. },
  1716. ref(['a', 'b', 'c']),
  1717. )
  1718. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1719. `
  1720. "
  1721. <!--[--><div>
  1722. <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
  1723. <div>1</div></div><div>3</div><!--]-->
  1724. "
  1725. `,
  1726. )
  1727. data.value.push('d')
  1728. await nextTick()
  1729. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1730. `
  1731. "
  1732. <!--[--><div>
  1733. <!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
  1734. <div>1</div></div><div>4</div><!--]-->
  1735. "
  1736. `,
  1737. )
  1738. })
  1739. test('v-for with insertion anchor', async () => {
  1740. const { container, data } = await testHydration(
  1741. `<template>
  1742. <div>
  1743. <span/>
  1744. <span v-for="item in data" :key="item">{{ item }}</span>
  1745. <span/>
  1746. </div>
  1747. </template>`,
  1748. undefined,
  1749. ref(['a', 'b', 'c']),
  1750. )
  1751. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1752. `
  1753. "<div><span></span>
  1754. <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
  1755. <span></span></div>"
  1756. `,
  1757. )
  1758. data.value.push('d')
  1759. await nextTick()
  1760. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1761. `
  1762. "<div><span></span>
  1763. <!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
  1764. <span></span></div>"
  1765. `,
  1766. )
  1767. data.value.splice(0, 1)
  1768. await nextTick()
  1769. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1770. `
  1771. "<div><span></span>
  1772. <!--[--><span>b</span><span>c</span><span>d</span><!--]-->
  1773. <span></span></div>"
  1774. `,
  1775. )
  1776. })
  1777. test('consecutive v-for with insertion anchor', async () => {
  1778. const { container, data } = await testHydration(
  1779. `<template>
  1780. <div>
  1781. <span/>
  1782. <span v-for="item in data" :key="item">{{ item }}</span>
  1783. <span v-for="item in data" :key="item">{{ item }}</span>
  1784. <span/>
  1785. </div>
  1786. </template>`,
  1787. undefined,
  1788. ref(['a', 'b', 'c']),
  1789. )
  1790. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1791. `
  1792. "<div><span></span>
  1793. <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
  1794. <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
  1795. <span></span></div>"
  1796. `,
  1797. )
  1798. data.value.push('d')
  1799. await nextTick()
  1800. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1801. `
  1802. "<div><span></span>
  1803. <!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
  1804. <!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
  1805. <span></span></div>"
  1806. `,
  1807. )
  1808. data.value.splice(0, 2)
  1809. await nextTick()
  1810. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1811. `
  1812. "<div><span></span>
  1813. <!--[--><span>c</span><span>d</span><!--]-->
  1814. <!--[--><span>c</span><span>d</span><!--]-->
  1815. <span></span></div>"
  1816. `,
  1817. )
  1818. })
  1819. test('v-for on component', async () => {
  1820. const { container, data } = await testHydration(
  1821. `<template>
  1822. <div>
  1823. <components.Child v-for="item in data" :key="item"/>
  1824. </div>
  1825. </template>`,
  1826. {
  1827. Child: `<template><div>comp</div></template>`,
  1828. },
  1829. ref(['a', 'b', 'c']),
  1830. )
  1831. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1832. `
  1833. "<div>
  1834. <!--[--><div>comp</div><div>comp</div><div>comp</div><!--]-->
  1835. </div>"
  1836. `,
  1837. )
  1838. data.value.push('d')
  1839. await nextTick()
  1840. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1841. `
  1842. "<div>
  1843. <!--[--><div>comp</div><div>comp</div><div>comp</div><div>comp</div><!--]-->
  1844. </div>"
  1845. `,
  1846. )
  1847. })
  1848. test('v-for on component with slots', async () => {
  1849. const { container, data } = await testHydration(
  1850. `<template>
  1851. <div>
  1852. <components.Child v-for="item in data" :key="item">
  1853. <span>{{ item }}</span>
  1854. </components.Child>
  1855. </div>
  1856. </template>`,
  1857. {
  1858. Child: `<template><slot/></template>`,
  1859. },
  1860. ref(['a', 'b', 'c']),
  1861. )
  1862. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1863. `
  1864. "<div>
  1865. <!--[-->
  1866. <!--[--><span>a</span><!--]-->
  1867. <!--[--><span>b</span><!--]-->
  1868. <!--[--><span>c</span><!--]-->
  1869. <!--]-->
  1870. </div>"
  1871. `,
  1872. )
  1873. data.value.push('d')
  1874. await nextTick()
  1875. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1876. `
  1877. "<div>
  1878. <!--[-->
  1879. <!--[--><span>a</span><!--]-->
  1880. <!--[--><span>b</span><!--]-->
  1881. <!--[--><span>c</span><!--]-->
  1882. <span>d</span><!--slot--><!--]-->
  1883. </div>"
  1884. `,
  1885. )
  1886. })
  1887. test('on fragment component', async () => {
  1888. const { container, data } = await testHydration(
  1889. `<template>
  1890. <div>
  1891. <components.Child v-for="item in data" :key="item"/>
  1892. </div>
  1893. </template>`,
  1894. {
  1895. Child: `<template><div>foo</div>-bar-</template>`,
  1896. },
  1897. ref(['a', 'b', 'c']),
  1898. )
  1899. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1900. `
  1901. "<div>
  1902. <!--[-->
  1903. <!--[--><div>foo</div>-bar-<!--]-->
  1904. <!--[--><div>foo</div>-bar-<!--]-->
  1905. <!--[--><div>foo</div>-bar-<!--]-->
  1906. <!--]-->
  1907. </div>"
  1908. `,
  1909. )
  1910. data.value.push('d')
  1911. await nextTick()
  1912. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1913. `
  1914. "<div>
  1915. <!--[-->
  1916. <!--[--><div>foo</div>-bar-<!--]-->
  1917. <!--[--><div>foo</div>-bar-<!--]-->
  1918. <!--[--><div>foo</div>-bar-<!--]-->
  1919. <div>foo</div>-bar-<!--]-->
  1920. </div>"
  1921. `,
  1922. )
  1923. })
  1924. test('on component with non-hydration node', async () => {
  1925. const data = ref({ show: true, msg: 'foo' })
  1926. const { container } = await testHydration(
  1927. `<template>
  1928. <div>
  1929. <components.Child v-for="item in 2" :key="item"/>
  1930. </div>
  1931. </template>`,
  1932. {
  1933. Child: `<template>
  1934. <div>
  1935. <div>
  1936. <div v-if="data.show">{{ data.msg }}</div>
  1937. </div>
  1938. <span>non-hydration node</span>
  1939. </div>
  1940. </template>`,
  1941. },
  1942. data,
  1943. )
  1944. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1945. `
  1946. "<div>
  1947. <!--[--><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><!--]-->
  1948. </div>"
  1949. `,
  1950. )
  1951. data.value.msg = 'bar'
  1952. await nextTick()
  1953. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1954. `
  1955. "<div>
  1956. <!--[--><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><!--]-->
  1957. </div>"
  1958. `,
  1959. )
  1960. data.value.show = false
  1961. await nextTick()
  1962. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  1963. "<div>
  1964. <!--[--><div><div><!--if--></div><span>non-hydration node</span></div><div><div><!--if--></div><span>non-hydration node</span></div><!--]-->
  1965. </div>"
  1966. `)
  1967. data.value.show = true
  1968. await nextTick()
  1969. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  1970. "<div>
  1971. <!--[--><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><!--]-->
  1972. </div>"
  1973. `)
  1974. })
  1975. test('with non-hydration node', async () => {
  1976. const data = ref({ show: true, msg: 'foo' })
  1977. const { container } = await testHydration(
  1978. `<template>
  1979. <div>
  1980. <div v-for="item in 2">
  1981. <div>
  1982. <div v-if="data.show">{{ data.msg }}</div>
  1983. </div>
  1984. <span>non-hydration node</span>
  1985. </div>
  1986. </div>
  1987. </template>`,
  1988. {},
  1989. data,
  1990. )
  1991. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1992. `
  1993. "<div>
  1994. <!--[--><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><!--]-->
  1995. </div>"
  1996. `,
  1997. )
  1998. data.value.msg = 'bar'
  1999. await nextTick()
  2000. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2001. `
  2002. "<div>
  2003. <!--[--><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><!--]-->
  2004. </div>"
  2005. `,
  2006. )
  2007. data.value.show = false
  2008. await nextTick()
  2009. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  2010. "<div>
  2011. <!--[--><div><div><!--if--></div><span>non-hydration node</span></div><div><div><!--if--></div><span>non-hydration node</span></div><!--]-->
  2012. </div>"
  2013. `)
  2014. data.value.show = true
  2015. await nextTick()
  2016. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  2017. "<div>
  2018. <!--[--><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><!--]-->
  2019. </div>"
  2020. `)
  2021. })
  2022. })
  2023. describe('slots', () => {
  2024. test('basic slot', async () => {
  2025. const { data, container } = await testHydration(
  2026. `<template>
  2027. <components.Child>
  2028. <span>{{data}}</span>
  2029. </components.Child>
  2030. </template>`,
  2031. {
  2032. Child: `<template><slot/></template>`,
  2033. },
  2034. )
  2035. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2036. `
  2037. "
  2038. <!--[--><span>foo</span><!--]-->
  2039. "
  2040. `,
  2041. )
  2042. data.value = 'bar'
  2043. await nextTick()
  2044. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2045. `
  2046. "
  2047. <!--[--><span>bar</span><!--]-->
  2048. "
  2049. `,
  2050. )
  2051. })
  2052. test('named slot', async () => {
  2053. const { data, container } = await testHydration(
  2054. `<template>
  2055. <components.Child>
  2056. <template #foo>
  2057. <span>{{data}}</span>
  2058. </template>
  2059. </components.Child>
  2060. </template>`,
  2061. {
  2062. Child: `<template><slot/><slot name="foo"/></template>`,
  2063. },
  2064. )
  2065. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2066. `
  2067. "
  2068. <!--[-->
  2069. <!--[--><!--]-->
  2070. <!--[--><span>foo</span><!--]-->
  2071. <!--]-->
  2072. "
  2073. `,
  2074. )
  2075. data.value = 'bar'
  2076. await nextTick()
  2077. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2078. `
  2079. "
  2080. <!--[-->
  2081. <!--[--><!--]-->
  2082. <!--[--><span>bar</span><!--]-->
  2083. <!--]-->
  2084. "
  2085. `,
  2086. )
  2087. })
  2088. test('named slot with v-if', async () => {
  2089. const { data, container } = await testHydration(
  2090. `<template>
  2091. <components.Child>
  2092. <template #foo v-if="data">
  2093. <span>{{data}}</span>
  2094. </template>
  2095. </components.Child>
  2096. </template>`,
  2097. {
  2098. Child: `<template><slot name="foo"/></template>`,
  2099. },
  2100. )
  2101. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2102. `
  2103. "
  2104. <!--[--><span>foo</span><!--]-->
  2105. "
  2106. `,
  2107. )
  2108. data.value = false
  2109. await nextTick()
  2110. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2111. `
  2112. "
  2113. <!--[--><!--]-->
  2114. "
  2115. `,
  2116. )
  2117. data.value = true
  2118. await nextTick()
  2119. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  2120. "
  2121. <!--[--><span>true</span><!--]-->
  2122. "
  2123. `)
  2124. })
  2125. test('named slot with v-if and v-for', async () => {
  2126. const data = reactive({
  2127. show: true,
  2128. items: ['a', 'b', 'c'],
  2129. })
  2130. const { container } = await testHydration(
  2131. `<template>
  2132. <components.Child>
  2133. <template #foo v-if="data.show">
  2134. <span v-for="item in data.items" :key="item">{{item}}</span>
  2135. </template>
  2136. </components.Child>
  2137. </template>`,
  2138. {
  2139. Child: `<template><slot name="foo"/></template>`,
  2140. },
  2141. data,
  2142. )
  2143. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2144. `
  2145. "
  2146. <!--[-->
  2147. <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
  2148. <!--]-->
  2149. "
  2150. `,
  2151. )
  2152. data.show = false
  2153. await nextTick()
  2154. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2155. `
  2156. "
  2157. <!--[-->
  2158. <!--[--><!--]-->
  2159. "
  2160. `,
  2161. )
  2162. data.show = true
  2163. await nextTick()
  2164. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2165. `
  2166. "
  2167. <!--[-->
  2168. <!--[--><span>a</span><span>b</span><span>c</span><!--for--><!--]-->
  2169. "
  2170. `,
  2171. )
  2172. })
  2173. test('with insertion anchor', async () => {
  2174. const { data, container } = await testHydration(
  2175. `<template>
  2176. <components.Child>
  2177. <span/>
  2178. <span>{{data}}</span>
  2179. <span/>
  2180. </components.Child>
  2181. </template>`,
  2182. {
  2183. Child: `<template><slot/></template>`,
  2184. },
  2185. )
  2186. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2187. `
  2188. "
  2189. <!--[--><span></span><span>foo</span><span></span><!--]-->
  2190. "
  2191. `,
  2192. )
  2193. data.value = 'bar'
  2194. await nextTick()
  2195. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2196. `
  2197. "
  2198. <!--[--><span></span><span>bar</span><span></span><!--]-->
  2199. "
  2200. `,
  2201. )
  2202. })
  2203. test('with multi level anchor insertion', async () => {
  2204. const { data, container } = await testHydration(
  2205. `<template>
  2206. <components.Child>
  2207. <span/>
  2208. <span>{{data}}</span>
  2209. <span/>
  2210. </components.Child>
  2211. </template>`,
  2212. {
  2213. Child: `
  2214. <template>
  2215. <div/>
  2216. <div/>
  2217. <slot/>
  2218. <div/>
  2219. </div>
  2220. </template>`,
  2221. },
  2222. )
  2223. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2224. `
  2225. "
  2226. <!--[--><div></div><div></div>
  2227. <!--[--><span></span><span>foo</span><span></span><!--]-->
  2228. <div></div><!--]-->
  2229. "
  2230. `,
  2231. )
  2232. data.value = 'bar'
  2233. await nextTick()
  2234. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2235. `
  2236. "
  2237. <!--[--><div></div><div></div>
  2238. <!--[--><span></span><span>bar</span><span></span><!--]-->
  2239. <div></div><!--]-->
  2240. "
  2241. `,
  2242. )
  2243. })
  2244. test('mixed slot and text node', async () => {
  2245. const data = reactive({
  2246. text: 'foo',
  2247. msg: 'hi',
  2248. })
  2249. const { container } = await testHydration(
  2250. `<template>
  2251. <components.Child>
  2252. <span>{{data.text}}</span>
  2253. </components.Child>
  2254. </template>`,
  2255. {
  2256. Child: `<template><div><slot/>{{data.msg}}</div></template>`,
  2257. },
  2258. data,
  2259. )
  2260. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2261. `
  2262. "<div>
  2263. <!--[--><span>foo</span><!--]-->
  2264. hi</div>"
  2265. `,
  2266. )
  2267. data.msg = 'bar'
  2268. await nextTick()
  2269. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2270. `
  2271. "<div>
  2272. <!--[--><span>foo</span><!--]-->
  2273. bar</div>"
  2274. `,
  2275. )
  2276. })
  2277. test('mixed root slot and text node', async () => {
  2278. const data = reactive({
  2279. text: 'foo',
  2280. msg: 'hi',
  2281. })
  2282. const { container } = await testHydration(
  2283. `<template>
  2284. <components.Child>
  2285. <span>{{data.text}}</span>
  2286. </components.Child>
  2287. </template>`,
  2288. {
  2289. Child: `<template>{{data.text}}<slot/>{{data.msg}}</template>`,
  2290. },
  2291. data,
  2292. )
  2293. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2294. `
  2295. "
  2296. <!--[-->foo
  2297. <!--[--><span>foo</span><!--]-->
  2298. hi<!--]-->
  2299. "
  2300. `,
  2301. )
  2302. data.msg = 'bar'
  2303. await nextTick()
  2304. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2305. `
  2306. "
  2307. <!--[-->foo
  2308. <!--[--><span>foo</span><!--]-->
  2309. bar<!--]-->
  2310. "
  2311. `,
  2312. )
  2313. })
  2314. test('mixed consecutive slot and element', async () => {
  2315. const data = reactive({
  2316. text: 'foo',
  2317. msg: 'hi',
  2318. })
  2319. const { container } = await testHydration(
  2320. `<template>
  2321. <components.Child>
  2322. <template #foo><span>{{data.text}}</span></template>
  2323. <template #bar><span>bar</span></template>
  2324. </components.Child>
  2325. </template>`,
  2326. {
  2327. Child: `<template><div><slot name="foo"/><slot name="bar"/><div>{{data.msg}}</div></div></template>`,
  2328. },
  2329. data,
  2330. )
  2331. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2332. `
  2333. "<div>
  2334. <!--[--><span>foo</span><!--]-->
  2335. <!--[--><span>bar</span><!--]-->
  2336. <div>hi</div></div>"
  2337. `,
  2338. )
  2339. data.msg = 'bar'
  2340. await nextTick()
  2341. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2342. `
  2343. "<div>
  2344. <!--[--><span>foo</span><!--]-->
  2345. <!--[--><span>bar</span><!--]-->
  2346. <div>bar</div></div>"
  2347. `,
  2348. )
  2349. })
  2350. test('mixed slot and element', async () => {
  2351. const data = reactive({
  2352. text: 'foo',
  2353. msg: 'hi',
  2354. })
  2355. const { container } = await testHydration(
  2356. `<template>
  2357. <components.Child>
  2358. <span>{{data.text}}</span>
  2359. </components.Child>
  2360. </template>`,
  2361. {
  2362. Child: `<template><div><slot/><div>{{data.msg}}</div></div></template>`,
  2363. },
  2364. data,
  2365. )
  2366. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2367. `
  2368. "<div>
  2369. <!--[--><span>foo</span><!--]-->
  2370. <div>hi</div></div>"
  2371. `,
  2372. )
  2373. data.msg = 'bar'
  2374. await nextTick()
  2375. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2376. `
  2377. "<div>
  2378. <!--[--><span>foo</span><!--]-->
  2379. <div>bar</div></div>"
  2380. `,
  2381. )
  2382. })
  2383. test('mixed slot and component', async () => {
  2384. const data = reactive({
  2385. msg1: 'foo',
  2386. msg2: 'bar',
  2387. })
  2388. const { container } = await testHydration(
  2389. `<template>
  2390. <components.Child>
  2391. <span>{{data.msg1}}</span>
  2392. </components.Child>
  2393. </template>`,
  2394. {
  2395. Child: `
  2396. <template>
  2397. <div>
  2398. <components.Child2/>
  2399. <slot/>
  2400. <components.Child2/>
  2401. </div>
  2402. </template>`,
  2403. Child2: `
  2404. <template>
  2405. <div>{{data.msg2}}</div>
  2406. </template>`,
  2407. },
  2408. data,
  2409. )
  2410. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2411. `
  2412. "<div><div>bar</div>
  2413. <!--[--><span>foo</span><!--]-->
  2414. <div>bar</div></div>"
  2415. `,
  2416. )
  2417. data.msg2 = 'hello'
  2418. await nextTick()
  2419. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2420. `
  2421. "<div><div>hello</div>
  2422. <!--[--><span>foo</span><!--]-->
  2423. <div>hello</div></div>"
  2424. `,
  2425. )
  2426. })
  2427. test('mixed slot and fragment component', async () => {
  2428. const data = reactive({
  2429. msg1: 'foo',
  2430. msg2: 'bar',
  2431. })
  2432. const { container } = await testHydration(
  2433. `<template>
  2434. <components.Child>
  2435. <span>{{data.msg1}}</span>
  2436. </components.Child>
  2437. </template>`,
  2438. {
  2439. Child: `
  2440. <template>
  2441. <div>
  2442. <components.Child2/>
  2443. <slot/>
  2444. <components.Child2/>
  2445. </div>
  2446. </template>`,
  2447. Child2: `
  2448. <template>
  2449. <div>{{data.msg1}}</div> {{data.msg2}}
  2450. </template>`,
  2451. },
  2452. data,
  2453. )
  2454. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2455. `
  2456. "<div>
  2457. <!--[--><div>foo</div> bar<!--]-->
  2458. <!--[--><span>foo</span><!--]-->
  2459. <!--[--><div>foo</div> bar<!--]-->
  2460. </div>"
  2461. `,
  2462. )
  2463. data.msg1 = 'hello'
  2464. data.msg2 = 'vapor'
  2465. await nextTick()
  2466. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2467. `
  2468. "<div>
  2469. <!--[--><div>hello</div> vapor<!--]-->
  2470. <!--[--><span>hello</span><!--]-->
  2471. <!--[--><div>hello</div> vapor<!--]-->
  2472. </div>"
  2473. `,
  2474. )
  2475. })
  2476. test('mixed slot and v-if', async () => {
  2477. const data = reactive({
  2478. show: true,
  2479. msg: 'foo',
  2480. })
  2481. const { container } = await testHydration(
  2482. `<template>
  2483. <components.Child>
  2484. <span>{{data.msg}}</span>
  2485. </components.Child>
  2486. </template>`,
  2487. {
  2488. Child: `
  2489. <template>
  2490. <div v-if="data.show">{{data.msg}}</div>
  2491. <slot/>
  2492. <div v-if="data.show">{{data.msg}}</div>
  2493. </template>`,
  2494. },
  2495. data,
  2496. )
  2497. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2498. `
  2499. "
  2500. <!--[--><div>foo</div><!--if-->
  2501. <!--[--><span>foo</span><!--]-->
  2502. <div>foo</div><!--if--><!--]-->
  2503. "
  2504. `,
  2505. )
  2506. data.show = false
  2507. await nextTick()
  2508. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2509. `
  2510. "
  2511. <!--[--><!--if-->
  2512. <!--[--><span>foo</span><!--]-->
  2513. <!--if--><!--]-->
  2514. "
  2515. `,
  2516. )
  2517. data.show = true
  2518. await nextTick()
  2519. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  2520. "
  2521. <!--[--><div>foo</div><!--if-->
  2522. <!--[--><span>foo</span><!--]-->
  2523. <div>foo</div><!--if--><!--]-->
  2524. "
  2525. `)
  2526. })
  2527. test('mixed slot and v-for', async () => {
  2528. const data = reactive({
  2529. items: ['a', 'b', 'c'],
  2530. msg: 'foo',
  2531. })
  2532. const { container } = await testHydration(
  2533. `<template>
  2534. <components.Child>
  2535. <span>{{data.msg}}</span>
  2536. </components.Child>
  2537. </template>`,
  2538. {
  2539. Child: `
  2540. <template>
  2541. <div v-for="item in data.items" :key="item">{{item}}</div>
  2542. <slot/>
  2543. <div v-for="item in data.items" :key="item">{{item}}</div>
  2544. </template>`,
  2545. },
  2546. data,
  2547. )
  2548. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2549. `
  2550. "
  2551. <!--[-->
  2552. <!--[--><div>a</div><div>b</div><div>c</div><!--]-->
  2553. <!--[--><span>foo</span><!--]-->
  2554. <!--[--><div>a</div><div>b</div><div>c</div><!--]-->
  2555. <!--]-->
  2556. "
  2557. `,
  2558. )
  2559. data.items.push('d')
  2560. await nextTick()
  2561. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2562. `
  2563. "
  2564. <!--[-->
  2565. <!--[--><div>a</div><div>b</div><div>c</div><div>d</div><!--]-->
  2566. <!--[--><span>foo</span><!--]-->
  2567. <!--[--><div>a</div><div>b</div><div>c</div><div>d</div><!--]-->
  2568. <!--]-->
  2569. "
  2570. `,
  2571. )
  2572. })
  2573. test('consecutive slots', async () => {
  2574. const data = reactive({
  2575. msg1: 'foo',
  2576. msg2: 'bar',
  2577. })
  2578. const { container } = await testHydration(
  2579. `<template>
  2580. <components.Child>
  2581. <span>{{data.msg1}}</span>
  2582. <template #bar>
  2583. <span>{{data.msg2}}</span>
  2584. </template>
  2585. </components.Child>
  2586. </template>`,
  2587. {
  2588. Child: `<template><slot/><slot name="bar"/></template>`,
  2589. },
  2590. data,
  2591. )
  2592. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2593. `
  2594. "
  2595. <!--[-->
  2596. <!--[--><span>foo</span><!--]-->
  2597. <!--[--><span>bar</span><!--]-->
  2598. <!--]-->
  2599. "
  2600. `,
  2601. )
  2602. data.msg1 = 'hello'
  2603. data.msg2 = 'vapor'
  2604. await nextTick()
  2605. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2606. `
  2607. "
  2608. <!--[-->
  2609. <!--[--><span>hello</span><!--]-->
  2610. <!--[--><span>vapor</span><!--]-->
  2611. <!--]-->
  2612. "
  2613. `,
  2614. )
  2615. })
  2616. test('consecutive slots with insertion anchor', async () => {
  2617. const data = reactive({
  2618. msg1: 'foo',
  2619. msg2: 'bar',
  2620. })
  2621. const { container } = await testHydration(
  2622. `<template>
  2623. <components.Child>
  2624. <span>{{data.msg1}}</span>
  2625. <template #bar>
  2626. <span>{{data.msg2}}</span>
  2627. </template>
  2628. </components.Child>
  2629. </template>`,
  2630. {
  2631. Child: `<template>
  2632. <div>
  2633. <span/>
  2634. <slot/>
  2635. <slot name="bar"/>
  2636. <span/>
  2637. </div>
  2638. </template>`,
  2639. },
  2640. data,
  2641. )
  2642. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2643. `
  2644. "<div><span></span>
  2645. <!--[--><span>foo</span><!--]-->
  2646. <!--[--><span>bar</span><!--]-->
  2647. <span></span></div>"
  2648. `,
  2649. )
  2650. data.msg1 = 'hello'
  2651. data.msg2 = 'vapor'
  2652. await nextTick()
  2653. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2654. `
  2655. "<div><span></span>
  2656. <!--[--><span>hello</span><!--]-->
  2657. <!--[--><span>vapor</span><!--]-->
  2658. <span></span></div>"
  2659. `,
  2660. )
  2661. })
  2662. test('consecutive slots prepend', async () => {
  2663. const data = reactive({
  2664. msg1: 'foo',
  2665. msg2: 'bar',
  2666. msg3: 'baz',
  2667. })
  2668. const { container } = await testHydration(
  2669. `<template>
  2670. <components.Child>
  2671. <template #foo>
  2672. <span>{{data.msg1}}</span>
  2673. </template>
  2674. <template #bar>
  2675. <span>{{data.msg2}}</span>
  2676. </template>
  2677. </components.Child>
  2678. </template>`,
  2679. {
  2680. Child: `<template>
  2681. <div>
  2682. <slot name="foo"/>
  2683. <slot name="bar"/>
  2684. <div>{{data.msg3}}</div>
  2685. </div>
  2686. </template>`,
  2687. },
  2688. data,
  2689. )
  2690. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2691. `
  2692. "<div>
  2693. <!--[--><span>foo</span><!--]-->
  2694. <!--[--><span>bar</span><!--]-->
  2695. <div>baz</div></div>"
  2696. `,
  2697. )
  2698. data.msg1 = 'hello'
  2699. data.msg2 = 'vapor'
  2700. await nextTick()
  2701. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2702. `
  2703. "<div>
  2704. <!--[--><span>hello</span><!--]-->
  2705. <!--[--><span>vapor</span><!--]-->
  2706. <div>baz</div></div>"
  2707. `,
  2708. )
  2709. })
  2710. test('slot fallback', async () => {
  2711. const data = reactive({
  2712. foo: 'foo',
  2713. })
  2714. const { container } = await testHydration(
  2715. `<template>
  2716. <components.Child>
  2717. </components.Child>
  2718. </template>`,
  2719. {
  2720. Child: `<template><slot><span>{{data.foo}}</span></slot></template>`,
  2721. },
  2722. data,
  2723. )
  2724. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2725. `
  2726. "
  2727. <!--[--><span>foo</span><!--]-->
  2728. "
  2729. `,
  2730. )
  2731. data.foo = 'bar'
  2732. await nextTick()
  2733. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2734. `
  2735. "
  2736. <!--[--><span>bar</span><!--]-->
  2737. "
  2738. `,
  2739. )
  2740. })
  2741. test('forwarded slot', async () => {
  2742. const data = reactive({
  2743. foo: 'foo',
  2744. bar: 'bar',
  2745. })
  2746. const { container } = await testHydration(
  2747. `<template>
  2748. <div>
  2749. <components.Parent>
  2750. <span>{{data.foo}}</span>
  2751. </components.Parent>
  2752. <div>{{data.bar}}</div>
  2753. </div>
  2754. </template>`,
  2755. {
  2756. Parent: `<template><div><components.Child><slot/></components.Child></div></template>`,
  2757. Child: `<template><div><slot/></div></template>`,
  2758. },
  2759. data,
  2760. )
  2761. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2762. `
  2763. "<div><div><div>
  2764. <!--[-->
  2765. <!--[--><span>foo</span><!--]-->
  2766. <!--]-->
  2767. </div></div><div>bar</div></div>"
  2768. `,
  2769. )
  2770. data.foo = 'foo1'
  2771. data.bar = 'bar1'
  2772. await nextTick()
  2773. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2774. `
  2775. "<div><div><div>
  2776. <!--[-->
  2777. <!--[--><span>foo1</span><!--]-->
  2778. <!--]-->
  2779. </div></div><div>bar1</div></div>"
  2780. `,
  2781. )
  2782. })
  2783. test('forwarded slot with fallback but rendered content', async () => {
  2784. const data = reactive({ foo: 'foo' })
  2785. const { container } = await testHydration(
  2786. `<template>
  2787. <components.Parent><span>{{data.foo}}</span></components.Parent>
  2788. </template>`,
  2789. {
  2790. Parent: `<template><components.Child><slot/></components.Child></template>`,
  2791. Child: `<template><div><slot>bar</slot></div></template>`,
  2792. },
  2793. data,
  2794. )
  2795. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  2796. "<div>
  2797. <!--[-->
  2798. <!--[--><span>foo</span><!--]-->
  2799. <!--]-->
  2800. </div>"
  2801. `)
  2802. data.foo = 'foo1'
  2803. await nextTick()
  2804. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  2805. "<div>
  2806. <!--[-->
  2807. <!--[--><span>foo1</span><!--]-->
  2808. <!--]-->
  2809. </div>"
  2810. `)
  2811. })
  2812. test('forwarded slot with fallback', async () => {
  2813. const data = reactive({
  2814. foo: 'foo',
  2815. })
  2816. const { container } = await testHydration(
  2817. `<template>
  2818. <components.Parent/>
  2819. </template>`,
  2820. {
  2821. Parent: `<template><components.Child><slot/></components.Child></template>`,
  2822. Child: `<template><div><slot>{{data.foo}}</slot></div></template>`,
  2823. },
  2824. data,
  2825. )
  2826. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2827. `
  2828. "<div>
  2829. <!--[-->foo<!--]-->
  2830. <!--slot--></div>"
  2831. `,
  2832. )
  2833. data.foo = 'foo1'
  2834. await nextTick()
  2835. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2836. `
  2837. "<div>
  2838. <!--[-->foo1<!--]-->
  2839. <!--slot--></div>"
  2840. `,
  2841. )
  2842. })
  2843. test('forwarded slot with empty content', async () => {
  2844. const data = reactive({
  2845. foo: 'foo',
  2846. })
  2847. const { container } = await testHydration(
  2848. `<template>
  2849. <components.Foo/>
  2850. </template>`,
  2851. {
  2852. Foo: `<template>
  2853. <components.Bar>
  2854. <template #foo>
  2855. <slot name="foo" />
  2856. </template>
  2857. </components.Bar>
  2858. </template>`,
  2859. Bar: `<template>
  2860. <components.Baz>
  2861. <template #foo>
  2862. <slot name="foo" />
  2863. </template>
  2864. </components.Baz>
  2865. </template>`,
  2866. Baz: `<template>
  2867. <components.Qux>
  2868. <template #foo>
  2869. <slot name="foo" />
  2870. </template>
  2871. </components.Qux>
  2872. </template>`,
  2873. Qux: `<template>
  2874. <div>
  2875. <slot name="foo" />
  2876. <div>{{data.foo}}</div>
  2877. </div>
  2878. </template>`,
  2879. },
  2880. data,
  2881. )
  2882. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2883. `
  2884. "<div>
  2885. <!--[--><!--]-->
  2886. <!--slot--><!--slot--><!--slot--><div>foo</div></div>"
  2887. `,
  2888. )
  2889. data.foo = 'bar'
  2890. await nextTick()
  2891. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2892. `
  2893. "<div>
  2894. <!--[--><!--]-->
  2895. <!--slot--><!--slot--><!--slot--><div>bar</div></div>"
  2896. `,
  2897. )
  2898. })
  2899. test('forwarded named slot can appear after hydrating as empty', async () => {
  2900. const data = reactive({
  2901. show: false,
  2902. foo: 'foo',
  2903. bar: 'bar',
  2904. })
  2905. const { container } = await testHydration(
  2906. `<template>
  2907. <components.Foo>
  2908. <template v-if="data.show" #foo>
  2909. <span>{{data.foo}}</span>
  2910. </template>
  2911. </components.Foo>
  2912. </template>`,
  2913. {
  2914. Foo: `<template>
  2915. <components.Bar>
  2916. <template #foo>
  2917. <slot name="foo" />
  2918. </template>
  2919. </components.Bar>
  2920. </template>`,
  2921. Bar: `<template>
  2922. <div>
  2923. <slot name="foo" />
  2924. <div>{{data.bar}}</div>
  2925. </div>
  2926. </template>`,
  2927. },
  2928. data,
  2929. )
  2930. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  2931. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2932. `
  2933. "<div>
  2934. <!--[--><!--]-->
  2935. <!--slot--><div>bar</div></div>"
  2936. `,
  2937. )
  2938. data.show = true
  2939. await nextTick()
  2940. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2941. `
  2942. "<div>
  2943. <!--[--><!--]-->
  2944. <span>foo</span><!--slot--><div>bar</div></div>"
  2945. `,
  2946. )
  2947. data.foo = 'foo1'
  2948. data.bar = 'bar1'
  2949. await nextTick()
  2950. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2951. `
  2952. "<div>
  2953. <!--[--><!--]-->
  2954. <span>foo1</span><!--slot--><div>bar1</div></div>"
  2955. `,
  2956. )
  2957. })
  2958. })
  2959. describe('transition', async () => {
  2960. test('transition appear', async () => {
  2961. const { container } = await testHydration(
  2962. `<template>
  2963. <transition appear>
  2964. <div>foo</div>
  2965. </transition>
  2966. </template>`,
  2967. )
  2968. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2969. `"<div style="" class="v-enter-from v-enter-active">foo</div>"`,
  2970. )
  2971. expect(`mismatch`).not.toHaveBeenWarned()
  2972. })
  2973. test('transition appear work with pre-existing class', async () => {
  2974. const { container } = await testHydration(
  2975. `<template>
  2976. <transition appear>
  2977. <div class="foo">foo</div>
  2978. </transition>
  2979. </template>`,
  2980. )
  2981. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2982. `"<div class="foo v-enter-from v-enter-active" style="">foo</div>"`,
  2983. )
  2984. expect(`mismatch`).not.toHaveBeenWarned()
  2985. })
  2986. test('transition appear work with empty content', async () => {
  2987. const data = ref(true)
  2988. const { container } = await testHydration(
  2989. `<template>
  2990. <transition appear>
  2991. <slot v-if="data"></slot>
  2992. <span v-else>foo</span>
  2993. </transition>
  2994. </template>`,
  2995. undefined,
  2996. data,
  2997. )
  2998. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2999. `"<!--slot--><!--if-->"`,
  3000. )
  3001. expect(`mismatch`).not.toHaveBeenWarned()
  3002. data.value = false
  3003. await nextTick()
  3004. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  3005. `"<span class="v-enter-from v-enter-active">foo</span><!--if-->"`,
  3006. )
  3007. })
  3008. test('transition appear with v-if', async () => {
  3009. const data = ref(false)
  3010. const { container } = await testHydration(
  3011. `<template>
  3012. <transition appear>
  3013. <div v-if="data">foo</div>
  3014. </transition>
  3015. </template>`,
  3016. undefined,
  3017. data,
  3018. )
  3019. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  3020. `"<!--if-->"`,
  3021. )
  3022. expect(`mismatch`).not.toHaveBeenWarned()
  3023. })
  3024. test('transition appear with v-show', async () => {
  3025. const data = ref(false)
  3026. const { container } = await testHydration(
  3027. `<template>
  3028. <transition appear>
  3029. <div v-show="data">foo</div>
  3030. </transition>
  3031. </template>`,
  3032. undefined,
  3033. data,
  3034. )
  3035. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  3036. `"<div style="display:none;" class="v-enter-from v-enter-active">foo</div>"`,
  3037. )
  3038. expect(`mismatch`).not.toHaveBeenWarned()
  3039. })
  3040. test('transition appear with slotted v-show', async () => {
  3041. const data = ref(false)
  3042. const { container } = await testHydration(
  3043. `<template>
  3044. <transition appear>
  3045. <components.Child>
  3046. <div v-show="data">foo</div>
  3047. </components.Child>
  3048. </transition>
  3049. </template>`,
  3050. {
  3051. Child: `<template><slot /></template>`,
  3052. },
  3053. data,
  3054. )
  3055. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  3056. `
  3057. "
  3058. <!--[--><div style="display:none;" class="v-enter-from v-enter-active">foo</div><!--]-->
  3059. "
  3060. `,
  3061. )
  3062. expect(`mismatch`).not.toHaveBeenWarned()
  3063. })
  3064. test('transition appear with forwarded slotted v-show', async () => {
  3065. const data = ref(false)
  3066. const { container } = await testHydration(
  3067. `<template>
  3068. <transition appear>
  3069. <components.Parent>
  3070. <div v-show="data">foo</div>
  3071. </components.Parent>
  3072. </transition>
  3073. </template>`,
  3074. {
  3075. Parent: `<template><components.Child><slot /></components.Child></template>`,
  3076. Child: `<template><slot /></template>`,
  3077. },
  3078. data,
  3079. )
  3080. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  3081. `
  3082. "
  3083. <!--[-->
  3084. <!--[--><div style="display:none;" class="v-enter-from v-enter-active">foo</div><!--]-->
  3085. <!--]-->
  3086. "
  3087. `,
  3088. )
  3089. expect(`mismatch`).not.toHaveBeenWarned()
  3090. })
  3091. test('transition appear w/ event listener', async () => {
  3092. const { container } = await testHydration(
  3093. `<script setup>
  3094. import { ref } from 'vue'
  3095. const count = ref(0)
  3096. </script>
  3097. <template>
  3098. <transition appear>
  3099. <button @click="count++">{{ count }}</button>
  3100. </transition>
  3101. </template>`,
  3102. )
  3103. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  3104. `"<button style="" class="v-enter-from v-enter-active">0</button>"`,
  3105. )
  3106. triggerEvent('click', container.querySelector('button')!)
  3107. await nextTick()
  3108. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  3109. `"<button style="" class="v-enter-from v-enter-active">1</button>"`,
  3110. )
  3111. })
  3112. })
  3113. describe('teleport', () => {
  3114. test('basic', async () => {
  3115. const data = ref({
  3116. msg: ref('foo'),
  3117. disabled: ref(false),
  3118. fn: vi.fn(),
  3119. })
  3120. const teleportContainer = document.createElement('div')
  3121. teleportContainer.id = 'teleport'
  3122. teleportContainer.innerHTML =
  3123. `<!--teleport start anchor-->` +
  3124. `<span>foo</span>` +
  3125. `<span class="foo"></span>` +
  3126. `<!--teleport anchor-->`
  3127. document.body.appendChild(teleportContainer)
  3128. const { block, container } = await mountWithHydration(
  3129. '<!--teleport start--><!--teleport end-->',
  3130. `<teleport to="#teleport" :disabled="data.disabled">
  3131. <span>{{data.msg}}</span>
  3132. <span :class="data.msg" @click="data.fn"></span>
  3133. </teleport>`,
  3134. data,
  3135. )
  3136. const teleport = block as TeleportFragment
  3137. expect(teleport.anchor).toBe(container.lastChild)
  3138. expect(teleport.target).toBe(teleportContainer)
  3139. expect(teleport.targetStart).toBe(teleportContainer.childNodes[0])
  3140. expect((teleport.nodes as Node[])[0]).toBe(
  3141. teleportContainer.childNodes[1],
  3142. )
  3143. expect((teleport.nodes as Node[])[1]).toBe(
  3144. teleportContainer.childNodes[2],
  3145. )
  3146. expect(teleport.targetAnchor).toBe(teleportContainer.childNodes[3])
  3147. expect(container.innerHTML).toMatchInlineSnapshot(
  3148. `"<!--teleport start--><!--teleport end-->"`,
  3149. )
  3150. // event handler
  3151. triggerEvent('click', teleportContainer.querySelector('.foo')!)
  3152. expect(data.value.fn).toHaveBeenCalled()
  3153. data.value.msg = 'bar'
  3154. await nextTick()
  3155. expect(formatHtml(teleportContainer.innerHTML)).toBe(
  3156. `<!--teleport start anchor-->` +
  3157. `<span>bar</span>` +
  3158. `<span class="bar"></span>` +
  3159. `<!--teleport anchor-->`,
  3160. )
  3161. data.value.disabled = true
  3162. await nextTick()
  3163. expect(container.innerHTML).toBe(
  3164. `<!--teleport start-->` +
  3165. `<span>bar</span>` +
  3166. `<span class="bar"></span>` +
  3167. `<!--teleport end-->`,
  3168. )
  3169. expect(formatHtml(teleportContainer.innerHTML)).toMatchInlineSnapshot(
  3170. `"<!--teleport start anchor--><!--teleport anchor-->"`,
  3171. )
  3172. data.value.msg = 'baz'
  3173. await nextTick()
  3174. expect(container.innerHTML).toBe(
  3175. `<!--teleport start-->` +
  3176. `<span>baz</span>` +
  3177. `<span class="baz"></span>` +
  3178. `<!--teleport end-->`,
  3179. )
  3180. data.value.disabled = false
  3181. await nextTick()
  3182. expect(container.innerHTML).toMatchInlineSnapshot(
  3183. `"<!--teleport start--><!--teleport end-->"`,
  3184. )
  3185. expect(formatHtml(teleportContainer.innerHTML)).toBe(
  3186. `<!--teleport start anchor-->` +
  3187. `<span>baz</span>` +
  3188. `<span class="baz"></span>` +
  3189. `<!--teleport anchor-->`,
  3190. )
  3191. })
  3192. test('multiple + integration', async () => {
  3193. const data = ref({
  3194. msg: ref('foo'),
  3195. fn1: vi.fn(),
  3196. fn2: vi.fn(),
  3197. })
  3198. const code = `
  3199. <teleport to="#teleport2">
  3200. <span>{{data.msg}}</span>
  3201. <span :class="data.msg" @click="data.fn1"></span>
  3202. </teleport>
  3203. <teleport to="#teleport2">
  3204. <span>{{data.msg}}2</span>
  3205. <span :class="data.msg + 2" @click="data.fn2"></span>
  3206. </teleport>`
  3207. const SSRComp = compileVaporComponent(code, data, undefined, true)
  3208. const teleportContainer = document.createElement('div')
  3209. teleportContainer.id = 'teleport2'
  3210. const ctx = {} as any
  3211. const mainHtml = await VueServerRenderer.renderToString(
  3212. runtimeDom.createSSRApp(SSRComp),
  3213. ctx,
  3214. )
  3215. expect(mainHtml).toBe(
  3216. `<!--[-->` +
  3217. `<!--teleport start--><!--teleport end-->` +
  3218. `<!--teleport start--><!--teleport end-->` +
  3219. `<!--]-->`,
  3220. )
  3221. const teleportHtml = ctx.teleports!['#teleport2']
  3222. expect(teleportHtml).toBe(
  3223. `<!--teleport start anchor-->` +
  3224. `<span>foo</span><span class="foo"></span>` +
  3225. `<!--teleport anchor-->` +
  3226. `<!--teleport start anchor-->` +
  3227. `<span>foo2</span><span class="foo2"></span>` +
  3228. `<!--teleport anchor-->`,
  3229. )
  3230. teleportContainer.innerHTML = teleportHtml
  3231. document.body.appendChild(teleportContainer)
  3232. const { block, container } = await mountWithHydration(
  3233. mainHtml,
  3234. code,
  3235. data,
  3236. )
  3237. const teleports = block as any as TeleportFragment[]
  3238. const teleport1 = teleports[0]
  3239. const teleport2 = teleports[1]
  3240. expect(teleport1.anchor).toBe(container.childNodes[2])
  3241. expect(teleport2.anchor).toBe(container.childNodes[4])
  3242. expect(teleport1.target).toBe(teleportContainer)
  3243. expect(teleport1.targetStart).toBe(teleportContainer.childNodes[0])
  3244. expect((teleport1.nodes as Node[])[0]).toBe(
  3245. teleportContainer.childNodes[1],
  3246. )
  3247. expect(teleport1.targetAnchor).toBe(teleportContainer.childNodes[3])
  3248. expect(teleport2.target).toBe(teleportContainer)
  3249. expect(teleport2.targetStart).toBe(teleportContainer.childNodes[4])
  3250. expect((teleport2.nodes as Node[])[0]).toBe(
  3251. teleportContainer.childNodes[5],
  3252. )
  3253. expect(teleport2.targetAnchor).toBe(teleportContainer.childNodes[7])
  3254. expect(container.innerHTML).toBe(
  3255. `<!--[-->` +
  3256. `<!--teleport start--><!--teleport end-->` +
  3257. `<!--teleport start--><!--teleport end-->` +
  3258. `<!--]-->`,
  3259. )
  3260. // event handler
  3261. triggerEvent('click', teleportContainer.querySelector('.foo')!)
  3262. expect(data.value.fn1).toHaveBeenCalled()
  3263. triggerEvent('click', teleportContainer.querySelector('.foo2')!)
  3264. expect(data.value.fn2).toHaveBeenCalled()
  3265. data.value.msg = 'bar'
  3266. await nextTick()
  3267. expect(teleportContainer.innerHTML).toBe(
  3268. `<!--teleport start anchor-->` +
  3269. `<span>bar</span>` +
  3270. `<span class="bar"></span>` +
  3271. `<!--teleport anchor-->` +
  3272. `<!--teleport start anchor-->` +
  3273. `<span>bar2</span>` +
  3274. `<span class="bar2"></span>` +
  3275. `<!--teleport anchor-->`,
  3276. )
  3277. })
  3278. test('disabled', async () => {
  3279. const data = ref({
  3280. msg: ref('foo'),
  3281. fn1: vi.fn(),
  3282. fn2: vi.fn(),
  3283. })
  3284. const code = `
  3285. <div>foo</div>
  3286. <teleport to="#teleport3" disabled="true">
  3287. <span>{{data.msg}}</span>
  3288. <span :class="data.msg" @click="data.fn1"></span>
  3289. </teleport>
  3290. <div :class="data.msg + 2" @click="data.fn2">bar</div>
  3291. `
  3292. const SSRComp = compileVaporComponent(code, data, undefined, true)
  3293. const teleportContainer = document.createElement('div')
  3294. teleportContainer.id = 'teleport3'
  3295. const ctx = {} as any
  3296. const mainHtml = await VueServerRenderer.renderToString(
  3297. runtimeDom.createSSRApp(SSRComp),
  3298. ctx,
  3299. )
  3300. expect(mainHtml).toBe(
  3301. `<!--[-->` +
  3302. `<div>foo</div>` +
  3303. `<!--teleport start-->` +
  3304. `<span>foo</span>` +
  3305. `<span class="foo"></span>` +
  3306. `<!--teleport end-->` +
  3307. `<div class="foo2">bar</div>` +
  3308. `<!--]-->`,
  3309. )
  3310. const teleportHtml = ctx.teleports!['#teleport3']
  3311. expect(teleportHtml).toMatchInlineSnapshot(
  3312. `"<!--teleport start anchor--><!--teleport anchor-->"`,
  3313. )
  3314. teleportContainer.innerHTML = teleportHtml
  3315. document.body.appendChild(teleportContainer)
  3316. const { block, container } = await mountWithHydration(
  3317. mainHtml,
  3318. code,
  3319. data,
  3320. )
  3321. const blocks = block as any[]
  3322. expect(blocks[0]).toBe(container.childNodes[1])
  3323. const teleport = blocks[1] as TeleportFragment
  3324. expect((teleport.nodes as Node[])[0]).toBe(container.childNodes[3])
  3325. expect((teleport.nodes as Node[])[1]).toBe(container.childNodes[4])
  3326. expect(teleport.anchor).toBe(container.childNodes[5])
  3327. expect(teleport.target).toBe(teleportContainer)
  3328. expect(teleport.targetStart).toBe(teleportContainer.childNodes[0])
  3329. expect(teleport.targetAnchor).toBe(teleportContainer.childNodes[1])
  3330. expect(blocks[2]).toBe(container.childNodes[6])
  3331. expect(container.innerHTML).toBe(
  3332. `<!--[-->` +
  3333. `<div>foo</div>` +
  3334. `<!--teleport start-->` +
  3335. `<span>foo</span>` +
  3336. `<span class="foo"></span>` +
  3337. `<!--teleport end-->` +
  3338. `<div class="foo2">bar</div>` +
  3339. `<!--]-->`,
  3340. )
  3341. // event handler
  3342. triggerEvent('click', container.querySelector('.foo')!)
  3343. expect(data.value.fn1).toHaveBeenCalled()
  3344. triggerEvent('click', container.querySelector('.foo2')!)
  3345. expect(data.value.fn2).toHaveBeenCalled()
  3346. data.value.msg = 'bar'
  3347. await nextTick()
  3348. expect(container.innerHTML).toBe(
  3349. `<!--[-->` +
  3350. `<div>foo</div>` +
  3351. `<!--teleport start-->` +
  3352. `<span>bar</span>` +
  3353. `<span class="bar"></span>` +
  3354. `<!--teleport end-->` +
  3355. `<div class="bar2">bar</div>` +
  3356. `<!--]-->`,
  3357. )
  3358. })
  3359. test('nested disabled teleport hydration should locate correct end anchor', async () => {
  3360. const data = ref({ msg: ref('after') })
  3361. const { block, container } = await mountWithHydration(
  3362. `<!--[-->` +
  3363. `<!--teleport start-->` +
  3364. `<div>outer</div>` +
  3365. `<!--teleport start-->` +
  3366. `<div>inner</div>` +
  3367. `<!--teleport end-->` +
  3368. `<!--teleport end-->` +
  3369. `<div>after</div>` +
  3370. `<!--]-->`,
  3371. `<teleport to="body" disabled>
  3372. <div>outer</div>
  3373. <teleport to="body" disabled>
  3374. <div>inner</div>
  3375. </teleport>
  3376. </teleport>
  3377. <div>{{data.msg}}</div>`,
  3378. data,
  3379. )
  3380. const blocks = block as any[]
  3381. const outerTeleport = blocks[0] as TeleportFragment
  3382. // The outer teleport's anchor must be the LAST <!--teleport end-->,
  3383. // NOT the inner one. If locateTeleportEndAnchor doesn't handle nesting,
  3384. // it would incorrectly pick the inner <!--teleport end-->.
  3385. const allEndComments = Array.from(container.childNodes).filter(
  3386. n => n.nodeType === 8 && (n as Comment).data === 'teleport end',
  3387. )
  3388. expect(allEndComments.length).toBe(2)
  3389. expect(outerTeleport.anchor).toBe(allEndComments[1]) // must be the LAST one
  3390. // The sibling <div>after</div> should hydrate correctly
  3391. // If the outer anchor is wrong, the hydration cursor is misaligned
  3392. // and the sibling element won't match.
  3393. expect(container.innerHTML).toBe(
  3394. `<!--[-->` +
  3395. `<!--teleport start-->` +
  3396. `<div>outer</div>` +
  3397. `<!--teleport start-->` +
  3398. `<div>inner</div>` +
  3399. `<!--teleport end-->` +
  3400. `<!--teleport end-->` +
  3401. `<div>after</div>` +
  3402. `<!--]-->`,
  3403. )
  3404. expect(`mismatch`).not.toHaveBeenWarned()
  3405. })
  3406. test('multiple disabled teleports hydrating to the same target should consume distinct target anchors', async () => {
  3407. const teleportContainer = document.createElement('div')
  3408. teleportContainer.id = 'teleport-disabled-shared'
  3409. teleportContainer.innerHTML =
  3410. `<!--teleport start anchor-->` +
  3411. `<!--teleport anchor-->` +
  3412. `<!--teleport start anchor-->` +
  3413. `<!--teleport anchor-->`
  3414. document.body.appendChild(teleportContainer)
  3415. const { block } = await mountWithHydration(
  3416. `<!--[-->` +
  3417. `<!--teleport start--><div>one</div><!--teleport end-->` +
  3418. `<!--teleport start--><div>two</div><!--teleport end-->` +
  3419. `<!--]-->`,
  3420. `<teleport to="#teleport-disabled-shared" disabled>
  3421. <div>one</div>
  3422. </teleport>
  3423. <teleport to="#teleport-disabled-shared" disabled>
  3424. <div>two</div>
  3425. </teleport>`,
  3426. )
  3427. const teleports = block as TeleportFragment[]
  3428. expect(teleports[0].targetStart).toBe(teleportContainer.childNodes[0])
  3429. expect(teleports[0].targetAnchor).toBe(teleportContainer.childNodes[1])
  3430. expect(teleports[1].targetStart).toBe(teleportContainer.childNodes[2])
  3431. expect(teleports[1].targetAnchor).toBe(teleportContainer.childNodes[3])
  3432. })
  3433. test('disabled + as component root', async () => {
  3434. const { container } = await mountWithHydration(
  3435. `<!--[-->` +
  3436. `<div>Parent fragment</div>` +
  3437. `<!--teleport start--><div>Teleport content</div><!--teleport end-->` +
  3438. `<!--]-->`,
  3439. `
  3440. <div>Parent fragment</div>
  3441. <teleport to="body" disabled>
  3442. <div>Teleport content</div>
  3443. </teleport>
  3444. `,
  3445. )
  3446. expect(container.innerHTML).toBe(
  3447. `<!--[-->` +
  3448. `<div>Parent fragment</div>` +
  3449. `<!--teleport start-->` +
  3450. `<div>Teleport content</div>` +
  3451. `<!--teleport end-->` +
  3452. `<!--]-->`,
  3453. )
  3454. expect(`mismatch`).not.toHaveBeenWarned()
  3455. })
  3456. test('as component root', async () => {
  3457. const teleportContainer = document.createElement('div')
  3458. teleportContainer.id = 'teleport4'
  3459. teleportContainer.innerHTML = `<!--teleport start anchor-->hello<!--teleport anchor-->`
  3460. document.body.appendChild(teleportContainer)
  3461. const { block, container } = await mountWithHydration(
  3462. '<!--teleport start--><!--teleport end-->',
  3463. `<components.Wrapper></components.Wrapper>`,
  3464. undefined,
  3465. {
  3466. Wrapper: compileVaporComponent(
  3467. `<teleport to="#teleport4">hello</teleport>`,
  3468. ),
  3469. },
  3470. )
  3471. const teleport = (block as VaporComponentInstance)
  3472. .block as TeleportFragment
  3473. expect(teleport.anchor).toBe(container.childNodes[1])
  3474. expect(teleport.target).toBe(teleportContainer)
  3475. expect(teleport.targetStart).toBe(teleportContainer.childNodes[0])
  3476. expect(teleport.nodes).toBe(teleportContainer.childNodes[1])
  3477. expect(teleport.targetAnchor).toBe(teleportContainer.childNodes[2])
  3478. })
  3479. test('nested', async () => {
  3480. const teleportContainer = document.createElement('div')
  3481. teleportContainer.id = 'teleport5'
  3482. teleportContainer.innerHTML =
  3483. `<!--teleport start anchor-->` +
  3484. `<!--teleport start--><!--teleport end-->` +
  3485. `<!--teleport anchor-->` +
  3486. `<!--teleport start anchor-->` +
  3487. `<div>child</div>` +
  3488. `<!--teleport anchor-->`
  3489. document.body.appendChild(teleportContainer)
  3490. const { block, container } = await mountWithHydration(
  3491. '<!--teleport start--><!--teleport end-->',
  3492. `<teleport to="#teleport5">
  3493. <teleport to="#teleport5"><div>child</div></teleport>
  3494. </teleport>`,
  3495. )
  3496. const teleport = block as TeleportFragment
  3497. expect(teleport.anchor).toBe(container.childNodes[1])
  3498. expect(teleport.targetStart).toBe(teleportContainer.childNodes[0])
  3499. expect(teleport.targetAnchor).toBe(teleportContainer.childNodes[3])
  3500. const childTeleport = teleport.nodes as TeleportFragment
  3501. expect(childTeleport.anchor).toBe(teleportContainer.childNodes[2])
  3502. expect(childTeleport.targetStart).toBe(teleportContainer.childNodes[4])
  3503. expect(childTeleport.targetAnchor).toBe(teleportContainer.childNodes[6])
  3504. expect(childTeleport.nodes).toBe(teleportContainer.childNodes[5])
  3505. })
  3506. test('unmount (full integration)', async () => {
  3507. const targetId = 'teleport6'
  3508. const data = ref({
  3509. toggle: ref(true),
  3510. })
  3511. const template1 = `<Teleport to="#${targetId}"><span>Teleported Comp1</span></Teleport>`
  3512. const Comp1 = compileVaporComponent(template1)
  3513. const SSRComp1 = compileVaporComponent(
  3514. template1,
  3515. undefined,
  3516. undefined,
  3517. true,
  3518. )
  3519. const template2 = `<div>Comp2</div>`
  3520. const Comp2 = compileVaporComponent(template2)
  3521. const SSRComp2 = compileVaporComponent(
  3522. template2,
  3523. undefined,
  3524. undefined,
  3525. true,
  3526. )
  3527. const appCode = `
  3528. <div>
  3529. <components.Comp1 v-if="data.toggle"/>
  3530. <components.Comp2 v-else/>
  3531. </div>
  3532. `
  3533. const SSRApp = compileVaporComponent(
  3534. appCode,
  3535. data,
  3536. {
  3537. Comp1: SSRComp1,
  3538. Comp2: SSRComp2,
  3539. },
  3540. true,
  3541. )
  3542. const teleportContainer = document.createElement('div')
  3543. teleportContainer.id = targetId
  3544. document.body.appendChild(teleportContainer)
  3545. const ctx = {} as any
  3546. const mainHtml = await VueServerRenderer.renderToString(
  3547. runtimeDom.createSSRApp(SSRApp),
  3548. ctx,
  3549. )
  3550. expect(mainHtml).toBe(
  3551. '<div><!--teleport start--><!--teleport end--></div>',
  3552. )
  3553. teleportContainer.innerHTML = ctx.teleports![`#${targetId}`]
  3554. const { container } = await mountWithHydration(mainHtml, appCode, data, {
  3555. Comp1,
  3556. Comp2,
  3557. })
  3558. expect(container.innerHTML).toBe(
  3559. '<div><!--teleport start--><!--teleport end--><!--if--></div>',
  3560. )
  3561. expect(teleportContainer.innerHTML).toBe(
  3562. `<!--teleport start anchor-->` +
  3563. `<span>Teleported Comp1</span>` +
  3564. `<!--teleport anchor-->`,
  3565. )
  3566. expect(`mismatch`).not.toHaveBeenWarned()
  3567. data.value.toggle = false
  3568. await nextTick()
  3569. expect(container.innerHTML).toBe('<div><div>Comp2</div><!--if--></div>')
  3570. expect(teleportContainer.innerHTML).toBe('')
  3571. })
  3572. test('unmount (mismatch + full integration)', async () => {
  3573. const targetId = 'teleport7'
  3574. const data = ref({
  3575. toggle: ref(true),
  3576. })
  3577. const template1 = `<Teleport to="#${targetId}"><span>Teleported Comp1</span></Teleport>`
  3578. const Comp1 = compileVaporComponent(template1)
  3579. const SSRComp1 = compileVaporComponent(
  3580. template1,
  3581. undefined,
  3582. undefined,
  3583. true,
  3584. )
  3585. const template2 = `<div>Comp2</div>`
  3586. const Comp2 = compileVaporComponent(template2)
  3587. const SSRComp2 = compileVaporComponent(
  3588. template2,
  3589. undefined,
  3590. undefined,
  3591. true,
  3592. )
  3593. const appCode = `
  3594. <div>
  3595. <components.Comp1 v-if="data.toggle"/>
  3596. <components.Comp2 v-else/>
  3597. </div>
  3598. `
  3599. const SSRApp = compileVaporComponent(
  3600. appCode,
  3601. data,
  3602. {
  3603. Comp1: SSRComp1,
  3604. Comp2: SSRComp2,
  3605. },
  3606. true,
  3607. )
  3608. const teleportContainer = document.createElement('div')
  3609. teleportContainer.id = targetId
  3610. document.body.appendChild(teleportContainer)
  3611. const mainHtml = await VueServerRenderer.renderToString(
  3612. runtimeDom.createSSRApp(SSRApp),
  3613. )
  3614. expect(mainHtml).toBe(
  3615. '<div><!--teleport start--><!--teleport end--></div>',
  3616. )
  3617. expect(teleportContainer.innerHTML).toBe('')
  3618. const { container } = await mountWithHydration(mainHtml, appCode, data, {
  3619. Comp1,
  3620. Comp2,
  3621. })
  3622. expect(container.innerHTML).toBe(
  3623. '<div><!--teleport start--><!--teleport end--><!--if--></div>',
  3624. )
  3625. expect(teleportContainer.innerHTML).toBe(`<span>Teleported Comp1</span>`)
  3626. expect(`Hydration children mismatch`).toHaveBeenWarned()
  3627. data.value.toggle = false
  3628. await nextTick()
  3629. expect(container.innerHTML).toBe('<div><div>Comp2</div><!--if--></div>')
  3630. expect(teleportContainer.innerHTML).toBe('')
  3631. })
  3632. test('target change (mismatch + full integration)', async () => {
  3633. const targetId1 = 'teleport8-1'
  3634. const targetId2 = 'teleport8-2'
  3635. const data = ref({
  3636. target: ref(targetId1),
  3637. msg: ref('foo'),
  3638. })
  3639. const template = `<Teleport :to="'#' + data.target"><span>{{data.msg}}</span></Teleport>`
  3640. const Comp = compileVaporComponent(template, data)
  3641. const SSRComp = compileVaporComponent(template, data, undefined, true)
  3642. const teleportContainer1 = document.createElement('div')
  3643. teleportContainer1.id = targetId1
  3644. const teleportContainer2 = document.createElement('div')
  3645. teleportContainer2.id = targetId2
  3646. document.body.appendChild(teleportContainer1)
  3647. document.body.appendChild(teleportContainer2)
  3648. // server render
  3649. const mainHtml = await VueServerRenderer.renderToString(
  3650. runtimeDom.createSSRApp(SSRComp),
  3651. )
  3652. expect(mainHtml).toBe(`<!--teleport start--><!--teleport end-->`)
  3653. expect(teleportContainer1.innerHTML).toBe('')
  3654. expect(teleportContainer2.innerHTML).toBe('')
  3655. // hydrate
  3656. const { container } = await mountWithHydration(mainHtml, template, data, {
  3657. Comp,
  3658. })
  3659. expect(container.innerHTML).toBe(
  3660. `<!--teleport start--><!--teleport end-->`,
  3661. )
  3662. expect(teleportContainer1.innerHTML).toBe(`<span>foo</span>`)
  3663. expect(teleportContainer2.innerHTML).toBe('')
  3664. expect(`Hydration children mismatch`).toHaveBeenWarned()
  3665. data.value.target = targetId2
  3666. data.value.msg = 'bar'
  3667. await nextTick()
  3668. expect(container.innerHTML).toBe(
  3669. `<!--teleport start--><!--teleport end-->`,
  3670. )
  3671. expect(teleportContainer1.innerHTML).toBe('')
  3672. expect(teleportContainer2.innerHTML).toBe(`<span>bar</span>`)
  3673. })
  3674. test('with disabled teleport + undefined target', async () => {
  3675. const data = ref({
  3676. msg: ref('foo'),
  3677. })
  3678. const { container } = await mountWithHydration(
  3679. '<!--teleport start--><span>foo</span><!--teleport end-->',
  3680. `<teleport :to="undefined" :disabled="true">
  3681. <span>{{data.msg}}</span>
  3682. </teleport>`,
  3683. data,
  3684. )
  3685. expect(container.innerHTML).toBe(
  3686. `<!--teleport start--><span>foo</span><!--teleport end-->`,
  3687. )
  3688. data.value.msg = 'bar'
  3689. await nextTick()
  3690. expect(container.innerHTML).toBe(
  3691. `<!--teleport start--><span>bar</span><!--teleport end-->`,
  3692. )
  3693. })
  3694. test('disabled teleport with null target hydration', async () => {
  3695. const { block, container } = await mountWithHydration(
  3696. '<!--teleport start--><div>content</div><!--teleport end-->',
  3697. `<teleport :to="undefined" :disabled="true">
  3698. <div>content</div>
  3699. </teleport>`,
  3700. )
  3701. expect(container.innerHTML).toBe(
  3702. `<!--teleport start--><div>content</div><!--teleport end-->`,
  3703. )
  3704. expect(`mismatch`).not.toHaveBeenWarned()
  3705. // targetStart must NOT be set when there's no target
  3706. const teleport = block as TeleportFragment
  3707. expect(teleport.targetStart).toBeNull()
  3708. expect(teleport.targetAnchor).toBeNull()
  3709. })
  3710. test('enabled teleport with null target', async () => {
  3711. const { container } = await mountWithHydration(
  3712. '<!--teleport start--><!--teleport end-->',
  3713. `<teleport to="#non-existent-target-hydrate">
  3714. <div>content</div>
  3715. </teleport>`,
  3716. )
  3717. // Align with VDOM Teleport behavior
  3718. expect(container.innerHTML).toBe(
  3719. `<!--teleport start--><!--teleport end-->`,
  3720. )
  3721. expect('Failed to locate Teleport target').toHaveBeenWarned()
  3722. })
  3723. test('enabled teleport with null target should delay child setup until target becomes available', async () => {
  3724. const version = ref('one')
  3725. const target = ref<any>('#non-existent-target-hydrate-late')
  3726. const setups: string[] = []
  3727. const Child = defineVaporComponent({
  3728. props: { msg: String },
  3729. setup(props) {
  3730. setups.push(String(props.msg))
  3731. const n0 = template('<div> </div>')() as any
  3732. const x0 = child(n0) as any
  3733. renderEffect(() => setText(x0, String(props.msg)))
  3734. return n0
  3735. },
  3736. })
  3737. const App = defineVaporComponent({
  3738. setup() {
  3739. return createComponent(
  3740. VaporTeleport,
  3741. { to: () => target.value },
  3742. {
  3743. default: () => {
  3744. const current = version.value
  3745. return createComponent(Child, { msg: () => current })
  3746. },
  3747. },
  3748. )
  3749. },
  3750. })
  3751. const container = document.createElement('div')
  3752. container.innerHTML = '<!--teleport start--><!--teleport end-->'
  3753. document.body.appendChild(container)
  3754. const app = createVaporSSRApp(App)
  3755. app.mount(container)
  3756. expect(container.innerHTML).toBe(
  3757. `<!--teleport start--><!--teleport end-->`,
  3758. )
  3759. expect(setups).toEqual([])
  3760. expect('Failed to locate Teleport target').toHaveBeenWarned()
  3761. version.value = 'two'
  3762. await nextTick()
  3763. version.value = 'three'
  3764. await nextTick()
  3765. expect(setups).toEqual([])
  3766. const targetEl = document.createElement('div')
  3767. target.value = targetEl
  3768. await nextTick()
  3769. expect(setups).toEqual(['three'])
  3770. expect(targetEl.innerHTML).toBe('<div>three</div>')
  3771. })
  3772. test('should apply css vars after hydration', async () => {
  3773. const state = reactive({ color: 'red' })
  3774. const teleportContainer = document.createElement('div')
  3775. teleportContainer.id = 'teleport-css-vars'
  3776. teleportContainer.innerHTML =
  3777. `<!--teleport start anchor-->` +
  3778. `<span>content</span>` +
  3779. `<!--teleport anchor-->`
  3780. document.body.appendChild(teleportContainer)
  3781. const App = defineVaporComponent({
  3782. setup() {
  3783. useVaporCssVars(() => state)
  3784. return createComponent(
  3785. VaporTeleport,
  3786. { to: () => '#teleport-css-vars' },
  3787. { default: () => template('<span>content</span>', true)() },
  3788. )
  3789. },
  3790. })
  3791. const container = document.createElement('div')
  3792. container.innerHTML = '<!--teleport start--><!--teleport end-->'
  3793. document.body.appendChild(container)
  3794. const app = createVaporSSRApp(App)
  3795. app.mount(container)
  3796. await nextTick()
  3797. // css vars should be applied after hydration
  3798. const span = teleportContainer.querySelector('span') as HTMLElement
  3799. expect(span).toBeTruthy()
  3800. expect(span.style.getPropertyValue('--color')).toBe('red')
  3801. expect(span.hasAttribute('data-v-owner')).toBe(true)
  3802. // css vars should update reactively
  3803. state.color = 'green'
  3804. await nextTick()
  3805. expect(span.style.getPropertyValue('--color')).toBe('green')
  3806. })
  3807. })
  3808. describe('async component', async () => {
  3809. test('async component', async () => {
  3810. const data = ref({
  3811. spy: vi.fn(),
  3812. })
  3813. const compCode = `<button @click="data.spy">hello!</button>`
  3814. const SSRComp = compileVaporComponent(compCode, data, undefined, true)
  3815. let serverResolve: any
  3816. // use defineAsyncComponent in SSR
  3817. let AsyncComp = defineAsyncComponent(
  3818. () =>
  3819. new Promise(r => {
  3820. serverResolve = r
  3821. }),
  3822. )
  3823. const appCode = `hello<components.AsyncComp/>world`
  3824. const SSRApp = compileVaporComponent(appCode, data, { AsyncComp }, true)
  3825. // server render
  3826. const htmlPromise = VueServerRenderer.renderToString(
  3827. runtimeDom.createSSRApp(SSRApp),
  3828. )
  3829. serverResolve(SSRComp)
  3830. const html = await htmlPromise
  3831. expect(html).toMatchInlineSnapshot(
  3832. `"<!--[-->hello<button>hello!</button>world<!--]-->"`,
  3833. )
  3834. // hydration
  3835. let clientResolve: any
  3836. AsyncComp = defineVaporAsyncComponent(
  3837. () =>
  3838. new Promise(r => {
  3839. clientResolve = r
  3840. }),
  3841. ) as any
  3842. const Comp = compileVaporComponent(compCode, data)
  3843. const App = compileVaporComponent(appCode, data, { AsyncComp })
  3844. const container = document.createElement('div')
  3845. container.innerHTML = html
  3846. document.body.appendChild(container)
  3847. createVaporSSRApp(App).mount(container)
  3848. // hydration not complete yet
  3849. triggerEvent('click', container.querySelector('button')!)
  3850. expect(data.value.spy).not.toHaveBeenCalled()
  3851. // resolve
  3852. clientResolve(Comp)
  3853. await new Promise(r => setTimeout(r))
  3854. // should be hydrated now
  3855. triggerEvent('click', container.querySelector('button')!)
  3856. expect(data.value.spy).toHaveBeenCalled()
  3857. })
  3858. // No longer needed, parent component updates in vapor mode no longer
  3859. // cause child components to re-render
  3860. // test.todo('update async wrapper before resolve', async () => {})
  3861. test('update async component after parent mount before async component resolve', async () => {
  3862. const data = ref({
  3863. toggle: true,
  3864. })
  3865. const compCode = `
  3866. <script vapor>
  3867. defineProps(['toggle'])
  3868. </script>
  3869. <template>
  3870. <h1>{{ toggle ? 'Async component' : 'Updated async component' }}</h1>
  3871. </template>
  3872. `
  3873. const SSRComp = compileVaporComponent(
  3874. compCode,
  3875. undefined,
  3876. undefined,
  3877. true,
  3878. )
  3879. let serverResolve: any
  3880. // use defineAsyncComponent in SSR
  3881. let AsyncComp = defineAsyncComponent(
  3882. () =>
  3883. new Promise(r => {
  3884. serverResolve = r
  3885. }),
  3886. )
  3887. const appCode = `<components.AsyncComp :toggle="data.toggle"/>`
  3888. const SSRApp = compileVaporComponent(appCode, data, { AsyncComp }, true)
  3889. // server render
  3890. const htmlPromise = VueServerRenderer.renderToString(
  3891. runtimeDom.createSSRApp(SSRApp),
  3892. )
  3893. serverResolve(SSRComp)
  3894. const html = await htmlPromise
  3895. expect(html).toMatchInlineSnapshot(`"<h1>Async component</h1>"`)
  3896. // hydration
  3897. let clientResolve: any
  3898. AsyncComp = defineVaporAsyncComponent(
  3899. () =>
  3900. new Promise(r => {
  3901. clientResolve = r
  3902. }),
  3903. ) as any
  3904. const Comp = compileVaporComponent(compCode)
  3905. const App = compileVaporComponent(appCode, data, { AsyncComp })
  3906. const container = document.createElement('div')
  3907. container.innerHTML = html
  3908. document.body.appendChild(container)
  3909. createVaporSSRApp(App).mount(container)
  3910. // update before resolve
  3911. data.value.toggle = false
  3912. await nextTick()
  3913. // resolve
  3914. clientResolve(Comp)
  3915. await new Promise(r => setTimeout(r))
  3916. // prevent lazy hydration since the component has been patched
  3917. expect('Skipping lazy hydration for component').toHaveBeenWarned()
  3918. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  3919. expect(container.innerHTML).toMatchInlineSnapshot(
  3920. `"<h1>Updated async component</h1><!--async component-->"`,
  3921. )
  3922. })
  3923. test('update async component (fragment root) after parent mount before async component resolve', async () => {
  3924. const data = ref({
  3925. toggle: true,
  3926. })
  3927. const compCode = `
  3928. <script vapor>
  3929. defineProps(['toggle'])
  3930. </script>
  3931. <template>
  3932. <h1>{{ toggle ? 'Async component' : 'Updated async component' }}</h1>
  3933. <h2>fragment root</h2>
  3934. </template>
  3935. `
  3936. const SSRComp = compileVaporComponent(
  3937. compCode,
  3938. undefined,
  3939. undefined,
  3940. true,
  3941. )
  3942. let serverResolve: any
  3943. // use defineAsyncComponent in SSR
  3944. let AsyncComp = defineAsyncComponent(
  3945. () =>
  3946. new Promise(r => {
  3947. serverResolve = r
  3948. }),
  3949. )
  3950. const appCode = `<components.AsyncComp :toggle="data.toggle"/>`
  3951. const SSRApp = compileVaporComponent(appCode, data, { AsyncComp }, true)
  3952. // server render
  3953. const htmlPromise = VueServerRenderer.renderToString(
  3954. runtimeDom.createSSRApp(SSRApp),
  3955. )
  3956. serverResolve(SSRComp)
  3957. const html = await htmlPromise
  3958. expect(html).toMatchInlineSnapshot(
  3959. `"<!--[--><h1>Async component</h1><h2>fragment root</h2><!--]-->"`,
  3960. )
  3961. // hydration
  3962. let clientResolve: any
  3963. AsyncComp = defineVaporAsyncComponent(
  3964. () =>
  3965. new Promise(r => {
  3966. clientResolve = r
  3967. }),
  3968. ) as any
  3969. const Comp = compileVaporComponent(compCode)
  3970. const App = compileVaporComponent(appCode, data, { AsyncComp })
  3971. const container = document.createElement('div')
  3972. container.innerHTML = html
  3973. document.body.appendChild(container)
  3974. createVaporSSRApp(App).mount(container)
  3975. // update before resolve
  3976. data.value.toggle = false
  3977. await nextTick()
  3978. // resolve
  3979. clientResolve(Comp)
  3980. await new Promise(r => setTimeout(r))
  3981. // prevent lazy hydration since the component has been patched
  3982. expect('Skipping lazy hydration for component').toHaveBeenWarned()
  3983. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  3984. expect(container.innerHTML).toMatchInlineSnapshot(
  3985. `"<!--[--><h1>Updated async component</h1><h2>fragment root</h2><!--async component--><!--]-->"`,
  3986. )
  3987. })
  3988. // required vapor Suspense
  3989. test.todo('hydrate safely when property used by async setup changed before render', async () => {})
  3990. // required vapor Suspense
  3991. test.todo('hydrate safely when property used by deep nested async setup changed before render', async () => {})
  3992. test('unmount async wrapper before load', async () => {
  3993. const data = ref({
  3994. toggle: true,
  3995. })
  3996. const compCode = `<div>async</div>`
  3997. const appCode = `
  3998. <div>
  3999. <components.AsyncComp v-if="data.toggle"/>
  4000. <div v-else>hi</div>
  4001. </div>
  4002. `
  4003. // hydration
  4004. let clientResolve: any
  4005. const AsyncComp = defineVaporAsyncComponent(
  4006. () =>
  4007. new Promise(r => {
  4008. clientResolve = r
  4009. }),
  4010. )
  4011. const Comp = compileVaporComponent(compCode)
  4012. const App = compileVaporComponent(appCode, data, {
  4013. AsyncComp,
  4014. })
  4015. const container = document.createElement('div')
  4016. container.innerHTML = '<div><div>async</div></div>'
  4017. createVaporSSRApp(App).mount(container)
  4018. // unmount before resolve
  4019. data.value.toggle = false
  4020. await nextTick()
  4021. expect(container.innerHTML).toBe(`<div><div>hi</div><!--if--></div>`)
  4022. // resolve
  4023. clientResolve(Comp)
  4024. await new Promise(r => setTimeout(r))
  4025. // should remain unmounted
  4026. expect(container.innerHTML).toBe(`<div><div>hi</div><!--if--></div>`)
  4027. })
  4028. test('unmount async wrapper before load (fragment)', async () => {
  4029. const data = ref({
  4030. toggle: true,
  4031. })
  4032. const compCode = `<div>async</div><div>fragment</div>`
  4033. const appCode = `
  4034. <div>
  4035. <components.AsyncComp v-if="data.toggle"/>
  4036. <div v-else>hi</div>
  4037. </div>
  4038. `
  4039. // hydration
  4040. let clientResolve: any
  4041. const AsyncComp = defineVaporAsyncComponent(
  4042. () =>
  4043. new Promise(r => {
  4044. clientResolve = r
  4045. }),
  4046. )
  4047. const Comp = compileVaporComponent(compCode)
  4048. const App = compileVaporComponent(appCode, data, {
  4049. AsyncComp,
  4050. })
  4051. const container = document.createElement('div')
  4052. container.innerHTML =
  4053. '<div><!--[--><div>async</div><div>fragment</div><!--]--></div>'
  4054. createVaporSSRApp(App).mount(container)
  4055. // unmount before resolve
  4056. data.value.toggle = false
  4057. await nextTick()
  4058. expect(container.innerHTML).toBe(`<div><div>hi</div><!--if--></div>`)
  4059. // resolve
  4060. clientResolve(Comp)
  4061. await new Promise(r => setTimeout(r))
  4062. // should remain unmounted
  4063. expect(container.innerHTML).toBe(`<div><div>hi</div><!--if--></div>`)
  4064. })
  4065. test('nested async wrapper', async () => {
  4066. const toggleCode = `
  4067. <script vapor>
  4068. import { onMounted, ref, nextTick } from 'vue'
  4069. const show = ref(false)
  4070. onMounted(() => {
  4071. nextTick(() => {
  4072. show.value = true
  4073. })
  4074. })
  4075. </script>
  4076. <template>
  4077. <div v-show="show">
  4078. <slot />
  4079. </div>
  4080. </template>
  4081. `
  4082. const SSRToggle = compileVaporComponent(
  4083. toggleCode,
  4084. undefined,
  4085. undefined,
  4086. true,
  4087. )
  4088. const wrapperCode = `<slot/>`
  4089. const SSRWrapper = compileVaporComponent(
  4090. wrapperCode,
  4091. undefined,
  4092. undefined,
  4093. true,
  4094. )
  4095. const data = ref({
  4096. count: 0,
  4097. fn: vi.fn(),
  4098. })
  4099. const childCode = `
  4100. <script vapor>
  4101. import { onMounted } from 'vue'
  4102. const data = _data; const components = _components;
  4103. onMounted(() => {
  4104. data.value.fn()
  4105. data.value.count++
  4106. })
  4107. </script>
  4108. <template>
  4109. <div>{{data.count}}</div>
  4110. </template>
  4111. `
  4112. const SSRChild = compileVaporComponent(childCode, data, undefined, true)
  4113. const appCode = `
  4114. <components.Toggle>
  4115. <components.Wrapper>
  4116. <components.Wrapper>
  4117. <components.Child/>
  4118. </components.Wrapper>
  4119. </components.Wrapper>
  4120. </components.Toggle>
  4121. `
  4122. const SSRApp = compileVaporComponent(
  4123. appCode,
  4124. undefined,
  4125. {
  4126. Toggle: SSRToggle,
  4127. Wrapper: SSRWrapper,
  4128. Child: SSRChild,
  4129. },
  4130. true,
  4131. )
  4132. const root = document.createElement('div')
  4133. // server render
  4134. root.innerHTML = await VueServerRenderer.renderToString(
  4135. runtimeDom.createSSRApp(SSRApp),
  4136. )
  4137. expect(root.innerHTML).toMatchInlineSnapshot(
  4138. `"<div style="display:none;"><!--[--><!--[--><!--[--><div>0</div><!--]--><!--]--><!--]--></div>"`,
  4139. )
  4140. const Toggle = compileVaporComponent(toggleCode)
  4141. const Wrapper = compileVaporComponent(wrapperCode)
  4142. const Child = compileVaporComponent(childCode, data)
  4143. const App = compileVaporComponent(appCode, undefined, {
  4144. Toggle,
  4145. Wrapper,
  4146. Child,
  4147. })
  4148. // hydration
  4149. createVaporSSRApp(App).mount(root)
  4150. await nextTick()
  4151. await nextTick()
  4152. expect(root.innerHTML).toMatchInlineSnapshot(
  4153. `"<div style=""><!--[--><!--[--><!--[--><div>1</div><!--]--><!--]--><!--]--></div>"`,
  4154. )
  4155. expect(data.value.fn).toBeCalledTimes(1)
  4156. })
  4157. })
  4158. describe.todo('Suspense')
  4159. describe('force hydrate prop', async () => {
  4160. test('force hydrate prop with `.prop` modifier', async () => {
  4161. const { container } = await mountWithHydration(
  4162. '<input type="checkbox">',
  4163. `<input type="checkbox" .indeterminate="true"/>`,
  4164. )
  4165. expect((container.firstChild! as any).indeterminate).toBe(true)
  4166. })
  4167. test('force hydrate input v-model with non-string value bindings', async () => {
  4168. const { container } = await mountWithHydration(
  4169. '<input type="checkbox" value="true">',
  4170. `<input type="checkbox" :true-value="true"/>`,
  4171. )
  4172. expect((container.firstChild as any)._trueValue).toBe(true)
  4173. })
  4174. test('force hydrate checkbox with indeterminate', async () => {
  4175. const { container } = await mountWithHydration(
  4176. '<input type="checkbox" indeterminate/>',
  4177. `<input type="checkbox" :indeterminate="true"/>`,
  4178. )
  4179. expect((container.firstChild! as any).indeterminate).toBe(true)
  4180. })
  4181. test('force hydrate select option with non-string value bindings', async () => {
  4182. const { container } = await mountWithHydration(
  4183. '<select><option value="true">ok</option></select>',
  4184. `<select><option :value="true">ok</option></select>`,
  4185. )
  4186. expect((container.firstChild!.firstChild as any)._value).toBe(true)
  4187. })
  4188. test('force hydrate v-bind with .prop modifiers', async () => {
  4189. const { container } = await mountWithHydration(
  4190. '<div .foo="true"/>',
  4191. `<div v-bind="data"/>`,
  4192. ref({ '.foo': true }),
  4193. )
  4194. expect((container.firstChild! as any).foo).toBe(true)
  4195. })
  4196. test('force hydrate custom element with dynamic props', () => {
  4197. class MyElement extends HTMLElement {
  4198. foo = ''
  4199. constructor() {
  4200. super()
  4201. }
  4202. }
  4203. customElements.define('my-element-7203', MyElement)
  4204. const msg = ref('bar')
  4205. const container = document.createElement('div')
  4206. container.innerHTML = '<my-element-7203></my-element-7203>'
  4207. const app = createVaporSSRApp({
  4208. setup() {
  4209. return createPlainElement('my-element-7203', { foo: () => msg.value })
  4210. },
  4211. })
  4212. app.mount(container)
  4213. expect((container.firstChild as any).foo).toBe(msg.value)
  4214. })
  4215. })
  4216. })
  4217. describe('mismatch handling', () => {
  4218. test('text node', async () => {
  4219. const foo = ref('bar')
  4220. const { container } = await mountWithHydration(`foo`, `{{data}}`, foo)
  4221. expect(container.textContent).toBe('bar')
  4222. expect(`Hydration text mismatch`).toHaveBeenWarned()
  4223. })
  4224. test('element text content', async () => {
  4225. const data = ref({ textContent: 'bar' })
  4226. const { container } = await mountWithHydration(
  4227. `<div>foo</div>`,
  4228. `<div v-bind="data"></div>`,
  4229. data,
  4230. )
  4231. expect(container.innerHTML).toBe('<div>bar</div>')
  4232. expect(`Hydration text content mismatch`).toHaveBeenWarned()
  4233. })
  4234. // test('not enough children', () => {
  4235. // const { container } = mountWithHydration(`<div></div>`, () =>
  4236. // h('div', [h('span', 'foo'), h('span', 'bar')]),
  4237. // )
  4238. // expect(container.innerHTML).toBe(
  4239. // '<div><span>foo</span><span>bar</span></div>',
  4240. // )
  4241. // expect(`Hydration children mismatch`).toHaveBeenWarned()
  4242. // })
  4243. // test('too many children', () => {
  4244. // const { container } = mountWithHydration(
  4245. // `<div><span>foo</span><span>bar</span></div>`,
  4246. // () => h('div', [h('span', 'foo')]),
  4247. // )
  4248. // expect(container.innerHTML).toBe('<div><span>foo</span></div>')
  4249. // expect(`Hydration children mismatch`).toHaveBeenWarned()
  4250. // })
  4251. test('complete mismatch', async () => {
  4252. const data = ref('span')
  4253. const { container } = await mountWithHydration(
  4254. `<div>foo</div>`,
  4255. `<component :is="data">foo</component>`,
  4256. data,
  4257. )
  4258. expect(container.innerHTML).toBe('<span>foo</span><!--dynamic-component-->')
  4259. expect(`Hydration node mismatch`).toHaveBeenWarned()
  4260. })
  4261. // test('fragment mismatch removal', () => {
  4262. // const { container } = mountWithHydration(
  4263. // `<div><!--[--><div>foo</div><div>bar</div><!--]--></div>`,
  4264. // () => h('div', [h('span', 'replaced')]),
  4265. // )
  4266. // expect(container.innerHTML).toBe('<div><span>replaced</span></div>')
  4267. // expect(`Hydration node mismatch`).toHaveBeenWarned()
  4268. // })
  4269. // test('fragment not enough children', () => {
  4270. // const { container } = mountWithHydration(
  4271. // `<div><!--[--><div>foo</div><!--]--><div>baz</div></div>`,
  4272. // () => h('div', [[h('div', 'foo'), h('div', 'bar')], h('div', 'baz')]),
  4273. // )
  4274. // expect(container.innerHTML).toBe(
  4275. // '<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>',
  4276. // )
  4277. // expect(`Hydration node mismatch`).toHaveBeenWarned()
  4278. // })
  4279. // test('fragment too many children', () => {
  4280. // const { container } = mountWithHydration(
  4281. // `<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>`,
  4282. // () => h('div', [[h('div', 'foo')], h('div', 'baz')]),
  4283. // )
  4284. // expect(container.innerHTML).toBe(
  4285. // '<div><!--[--><div>foo</div><!--]--><div>baz</div></div>',
  4286. // )
  4287. // // fragment ends early and attempts to hydrate the extra <div>bar</div>
  4288. // // as 2nd fragment child.
  4289. // expect(`Hydration text content mismatch`).toHaveBeenWarned()
  4290. // // excessive children removal
  4291. // expect(`Hydration children mismatch`).toHaveBeenWarned()
  4292. // })
  4293. // test('Teleport target has empty children', () => {
  4294. // const teleportContainer = document.createElement('div')
  4295. // teleportContainer.id = 'teleport'
  4296. // document.body.appendChild(teleportContainer)
  4297. // mountWithHydration('<!--teleport start--><!--teleport end-->', () =>
  4298. // h(Teleport, { to: '#teleport' }, [h('span', 'value')]),
  4299. // )
  4300. // expect(teleportContainer.innerHTML).toBe(`<span>value</span>`)
  4301. // expect(`Hydration children mismatch`).toHaveBeenWarned()
  4302. // })
  4303. // test('comment mismatch (element)', () => {
  4304. // const { container } = mountWithHydration(`<div><span></span></div>`, () =>
  4305. // h('div', [createCommentVNode('hi')]),
  4306. // )
  4307. // expect(container.innerHTML).toBe('<div><!--hi--></div>')
  4308. // expect(`Hydration node mismatch`).toHaveBeenWarned()
  4309. // })
  4310. // test('comment mismatch (text)', () => {
  4311. // const { container } = mountWithHydration(`<div>foobar</div>`, () =>
  4312. // h('div', [createCommentVNode('hi')]),
  4313. // )
  4314. // expect(container.innerHTML).toBe('<div><!--hi--></div>')
  4315. // expect(`Hydration node mismatch`).toHaveBeenWarned()
  4316. // })
  4317. test('class mismatch', async () => {
  4318. await mountWithHydration(
  4319. `<div class="foo bar"></div>`,
  4320. `<div :class="data"></div>`,
  4321. ref(['foo', 'bar']),
  4322. )
  4323. await mountWithHydration(
  4324. `<div class="foo bar"></div>`,
  4325. `<div :class="data"></div>`,
  4326. ref({ foo: true, bar: true }),
  4327. )
  4328. await mountWithHydration(
  4329. `<div class="foo bar"></div>`,
  4330. `<div :class="data"></div>`,
  4331. ref('foo bar'),
  4332. )
  4333. // svg classes
  4334. await mountWithHydration(
  4335. `<svg class="foo bar"></svg>`,
  4336. `<svg :class="data"></svg>`,
  4337. ref('foo bar'),
  4338. )
  4339. // class with different order
  4340. await mountWithHydration(
  4341. `<div class="foo bar"></div>`,
  4342. `<div :class="data"></div>`,
  4343. ref('bar foo'),
  4344. )
  4345. expect(`Hydration class mismatch`).not.toHaveBeenWarned()
  4346. // single root mismatch
  4347. const { container: root } = await mountWithHydration(
  4348. `<div class="foo bar"></div>`,
  4349. `<div :class="data"></div>`,
  4350. ref('baz'),
  4351. )
  4352. expect(root.innerHTML).toBe('<div class="foo bar baz"></div>')
  4353. expect(`Hydration class mismatch`).toHaveBeenWarned()
  4354. // multiple root mismatch
  4355. const { container } = await mountWithHydration(
  4356. `<div class="foo bar"></div><span/>`,
  4357. `<div :class="data"></div><span/>`,
  4358. ref('foo'),
  4359. )
  4360. expect(container.innerHTML).toBe('<div class="foo"></div><span></span>')
  4361. expect(`Hydration class mismatch`).toHaveBeenWarned()
  4362. })
  4363. test('style mismatch', async () => {
  4364. await mountWithHydration(
  4365. `<div style="color:red;"></div>`,
  4366. `<div :style="data"></div>`,
  4367. ref({ color: 'red' }),
  4368. )
  4369. await mountWithHydration(
  4370. `<div style="color:red;"></div>`,
  4371. `<div :style="data"></div>`,
  4372. ref('color:red;'),
  4373. )
  4374. // style with different order
  4375. await mountWithHydration(
  4376. `<div style="color:red; font-size: 12px;"></div>`,
  4377. `<div :style="data"></div>`,
  4378. ref(`font-size: 12px; color:red;`),
  4379. )
  4380. expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  4381. // single root mismatch
  4382. const { container: root } = await mountWithHydration(
  4383. `<div style="color:red;"></div>`,
  4384. `<div :style="data"></div>`,
  4385. ref({ color: 'green' }),
  4386. )
  4387. expect(root.innerHTML).toBe('<div style="color: green;"></div>')
  4388. expect(`Hydration style mismatch`).toHaveBeenWarned()
  4389. // multiple root mismatch
  4390. const { container } = await mountWithHydration(
  4391. `<div style="color:red;"></div><span/>`,
  4392. `<div :style="data"></div><span/>`,
  4393. ref({ color: 'green' }),
  4394. )
  4395. expect(container.innerHTML).toBe(
  4396. '<div style="color: green;"></div><span></span>',
  4397. )
  4398. expect(`Hydration style mismatch`).toHaveBeenWarned()
  4399. })
  4400. test('style mismatch when no style attribute is present', async () => {
  4401. await mountWithHydration(
  4402. `<div></div>`,
  4403. `<div :style="data"></div>`,
  4404. ref({ color: 'red' }),
  4405. )
  4406. expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
  4407. })
  4408. test('style mismatch w/ v-show', async () => {
  4409. await mountWithHydration(
  4410. `<div style="color:red;display:none"></div>`,
  4411. `<div v-show="data" style="color: red;"></div>`,
  4412. ref(false),
  4413. )
  4414. expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  4415. // mismatch with single root
  4416. const { container: root } = await mountWithHydration(
  4417. `<div style="color:red;"></div>`,
  4418. `<div v-show="data" style="color: red;"></div>`,
  4419. ref(false),
  4420. )
  4421. expect(root.innerHTML).toBe(
  4422. '<div style="color: red; display: none;"></div>',
  4423. )
  4424. expect(`Hydration style mismatch`).toHaveBeenWarned()
  4425. // mismatch with multiple root
  4426. const { container } = await mountWithHydration(
  4427. `<div style="color:red;"></div><span/>`,
  4428. `<div v-show="data.show" :style="data.style"></div><span/>`,
  4429. ref({ show: false, style: 'color: red' }),
  4430. )
  4431. expect(container.innerHTML).toBe(
  4432. '<div style="color: red; display: none;"></div><span></span>',
  4433. )
  4434. expect(`Hydration style mismatch`).toHaveBeenWarned()
  4435. })
  4436. test('attr mismatch', async () => {
  4437. await mountWithHydration(
  4438. `<div id="foo"></div>`,
  4439. `<div :id="data"></div>`,
  4440. ref('foo'),
  4441. )
  4442. await mountWithHydration(
  4443. `<div spellcheck></div>`,
  4444. `<div :spellcheck="data"></div>`,
  4445. ref(''),
  4446. )
  4447. await mountWithHydration(
  4448. `<div></div>`,
  4449. `<div :id="data"></div>`,
  4450. ref(undefined),
  4451. )
  4452. // boolean
  4453. await mountWithHydration(
  4454. `<select multiple></div>`,
  4455. `<select :multiple="data"></select>`,
  4456. ref(true),
  4457. )
  4458. await mountWithHydration(
  4459. `<select multiple></div>`,
  4460. `<select :multiple="data"></select>`,
  4461. ref('multiple'),
  4462. )
  4463. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  4464. await mountWithHydration(
  4465. `<div></div>`,
  4466. `<div :id="data"></div>`,
  4467. ref('foo'),
  4468. )
  4469. expect(`Hydration attribute mismatch`).toHaveBeenWarnedTimes(1)
  4470. await mountWithHydration(
  4471. `<div id="bar"></div>`,
  4472. `<div :id="data"></div>`,
  4473. ref('foo'),
  4474. )
  4475. expect(`Hydration attribute mismatch`).toHaveBeenWarnedTimes(2)
  4476. })
  4477. test('attr special case: textarea value', async () => {
  4478. await mountWithHydration(
  4479. `<textarea>foo</textarea>`,
  4480. `<textarea :value="data"></textarea>`,
  4481. ref('foo'),
  4482. )
  4483. await mountWithHydration(
  4484. `<textarea></textarea>`,
  4485. `<textarea :value="data"></textarea>`,
  4486. ref(''),
  4487. )
  4488. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  4489. await mountWithHydration(
  4490. `<textarea>foo</textarea>`,
  4491. `<textarea :value="data"></textarea>`,
  4492. ref('bar'),
  4493. )
  4494. expect(`Hydration attribute mismatch`).toHaveBeenWarned()
  4495. })
  4496. test('<textarea> with newlines at the beginning', async () => {
  4497. await mountWithHydration(
  4498. `<textarea>\nhello</textarea>`,
  4499. `<textarea :value="data"></textarea>`,
  4500. ref('\nhello'),
  4501. )
  4502. await mountWithHydration(
  4503. `<textarea>\nhello</textarea>`,
  4504. `<textarea v-text="data"></textarea>`,
  4505. ref('\nhello'),
  4506. )
  4507. await mountWithHydration(
  4508. `<textarea>\nhello</textarea>`,
  4509. `<textarea v-bind="data"></textarea>`,
  4510. ref({ textContent: '\nhello' }),
  4511. )
  4512. expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
  4513. })
  4514. test('<pre> with newlines at the beginning', async () => {
  4515. await mountWithHydration(`<pre>\n</pre>`, `<pre>{{data}}</pre>`, ref('\n'))
  4516. await mountWithHydration(
  4517. `<pre>\n</pre>`,
  4518. `<pre v-text="data"></pre>`,
  4519. ref('\n'),
  4520. )
  4521. await mountWithHydration(
  4522. `<pre>\n</pre>`,
  4523. `<pre v-bind="data"></pre>`,
  4524. ref({ textContent: '\n' }),
  4525. )
  4526. expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
  4527. })
  4528. test('boolean attr handling', async () => {
  4529. await mountWithHydration(
  4530. `<input />`,
  4531. `<input :readonly="data" />`,
  4532. ref(false),
  4533. )
  4534. await mountWithHydration(
  4535. `<input readonly />`,
  4536. `<input :readonly="data" />`,
  4537. ref(true),
  4538. )
  4539. await mountWithHydration(
  4540. `<input readonly="readonly" />`,
  4541. `<input :readonly="data" />`,
  4542. ref(true),
  4543. )
  4544. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  4545. })
  4546. test('client value is null or undefined', async () => {
  4547. await mountWithHydration(
  4548. `<div></div>`,
  4549. `<div :draggable="data"></div>`,
  4550. ref(undefined),
  4551. )
  4552. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  4553. await mountWithHydration(`<input />`, `<input :type="data" />`, ref(null))
  4554. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  4555. })
  4556. test('should not warn against object values', async () => {
  4557. await mountWithHydration(`<input />`, `<input :from="data" />`, ref({}))
  4558. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  4559. })
  4560. test('should not warn on falsy bindings of non-property keys', async () => {
  4561. await mountWithHydration(
  4562. `<button></button>`,
  4563. `<button :href="data"></button>`,
  4564. ref(undefined),
  4565. )
  4566. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  4567. })
  4568. test('should not warn on non-renderable option values', async () => {
  4569. await mountWithHydration(
  4570. `<select><option>hello</option></select>`,
  4571. `<select><option :value="data">hello</option></select>`,
  4572. ref(['foo']),
  4573. )
  4574. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  4575. })
  4576. test('should not warn css v-bind', async () => {
  4577. const container = document.createElement('div')
  4578. container.innerHTML = `<div style="--foo:red;color:var(--foo);" />`
  4579. const app = createVaporSSRApp({
  4580. setup() {
  4581. useVaporCssVars(() => ({ foo: 'red' }))
  4582. const n0 = template('<div></div>', true)() as any
  4583. renderEffect(() => setStyle(n0, { color: 'var(--foo)' }))
  4584. return n0
  4585. },
  4586. })
  4587. app.mount(container)
  4588. expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  4589. })
  4590. test('css vars should only be added to expected on component root dom', () => {
  4591. const container = document.createElement('div')
  4592. container.innerHTML = `<div style="--foo:red;"><div style="color:var(--foo);" /></div>`
  4593. const app = createVaporSSRApp({
  4594. setup() {
  4595. useVaporCssVars(() => ({ foo: 'red' }))
  4596. const n0 = template('<div><div></div></div>', true)() as any
  4597. const n1 = child(n0) as any
  4598. renderEffect(() => setStyle(n1, { color: 'var(--foo)' }))
  4599. return n0
  4600. },
  4601. })
  4602. app.mount(container)
  4603. expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  4604. })
  4605. test('css vars support fallthrough', () => {
  4606. const container = document.createElement('div')
  4607. container.innerHTML = `<div style="padding: 4px;--foo:red;"></div>`
  4608. const app = createVaporSSRApp({
  4609. setup() {
  4610. useVaporCssVars(() => ({ foo: 'red' }))
  4611. return createComponent(Child)
  4612. },
  4613. })
  4614. const Child = defineVaporComponent({
  4615. setup() {
  4616. const n0 = template('<div></div>', true)() as any
  4617. renderEffect(() => setStyle(n0, { padding: '4px' }))
  4618. return n0
  4619. },
  4620. })
  4621. app.mount(container)
  4622. expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  4623. })
  4624. // vapor directive does not have a created hook
  4625. test('should not warn for directives that mutate DOM in created', () => {
  4626. // const container = document.createElement('div')
  4627. // container.innerHTML = `<div class="test red"></div>`
  4628. // const vColor: ObjectDirective = {
  4629. // created(el, binding) {
  4630. // el.classList.add(binding.value)
  4631. // },
  4632. // }
  4633. // const app = createSSRApp({
  4634. // setup() {
  4635. // return () =>
  4636. // withDirectives(h('div', { class: 'test' }), [[vColor, 'red']])
  4637. // },
  4638. // })
  4639. // app.mount(container)
  4640. // expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  4641. })
  4642. test('escape css var name', () => {
  4643. const container = document.createElement('div')
  4644. container.innerHTML = `<div style="padding: 4px;--foo\\.bar:red;"></div>`
  4645. const app = createVaporSSRApp({
  4646. setup() {
  4647. useVaporCssVars(() => ({ 'foo.bar': 'red' }))
  4648. return createComponent(Child)
  4649. },
  4650. })
  4651. const Child = defineVaporComponent({
  4652. setup() {
  4653. const n0 = template('<div></div>', true)() as any
  4654. renderEffect(() => setStyle(n0, { padding: '4px' }))
  4655. return n0
  4656. },
  4657. })
  4658. app.mount(container)
  4659. expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  4660. })
  4661. })
  4662. describe('data-allow-mismatch', () => {
  4663. test('element text content', async () => {
  4664. const data = ref({ textContent: 'bar' })
  4665. const { container } = await mountWithHydration(
  4666. `<div data-allow-mismatch="text">foo</div>`,
  4667. `<div v-bind="data"></div>`,
  4668. data,
  4669. )
  4670. expect(container.innerHTML).toBe(
  4671. '<div data-allow-mismatch="text">bar</div>',
  4672. )
  4673. expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
  4674. })
  4675. // test('not enough children', () => {
  4676. // const { container } = mountWithHydration(
  4677. // `<div data-allow-mismatch="children"></div>`,
  4678. // () => h('div', [h('span', 'foo'), h('span', 'bar')]),
  4679. // )
  4680. // expect(container.innerHTML).toBe(
  4681. // '<div data-allow-mismatch="children"><span>foo</span><span>bar</span></div>',
  4682. // )
  4683. // expect(`Hydration children mismatch`).not.toHaveBeenWarned()
  4684. // })
  4685. // test('too many children', () => {
  4686. // const { container } = mountWithHydration(
  4687. // `<div data-allow-mismatch="children"><span>foo</span><span>bar</span></div>`,
  4688. // () => h('div', [h('span', 'foo')]),
  4689. // )
  4690. // expect(container.innerHTML).toBe(
  4691. // '<div data-allow-mismatch="children"><span>foo</span></div>',
  4692. // )
  4693. // expect(`Hydration children mismatch`).not.toHaveBeenWarned()
  4694. // })
  4695. test('complete mismatch', async () => {
  4696. const { container } = await mountWithHydration(
  4697. `<div data-allow-mismatch="children"><div>foo</div></div>`,
  4698. `<div><component :is="data">foo</component></div>`,
  4699. ref('span'),
  4700. )
  4701. expect(container.innerHTML).toBe(
  4702. '<div data-allow-mismatch="children"><span>foo</span><!--dynamic-component--></div>',
  4703. )
  4704. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  4705. })
  4706. // test('fragment mismatch removal', () => {
  4707. // const { container } = mountWithHydration(
  4708. // `<div data-allow-mismatch="children"><!--[--><div>foo</div><div>bar</div><!--]--></div>`,
  4709. // () => h('div', [h('span', 'replaced')]),
  4710. // )
  4711. // expect(container.innerHTML).toBe(
  4712. // '<div data-allow-mismatch="children"><span>replaced</span></div>',
  4713. // )
  4714. // expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  4715. // })
  4716. // test('fragment not enough children', () => {
  4717. // const { container } = mountWithHydration(
  4718. // `<div data-allow-mismatch="children"><!--[--><div>foo</div><!--]--><div>baz</div></div>`,
  4719. // () => h('div', [[h('div', 'foo'), h('div', 'bar')], h('div', 'baz')]),
  4720. // )
  4721. // expect(container.innerHTML).toBe(
  4722. // '<div data-allow-mismatch="children"><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>',
  4723. // )
  4724. // expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  4725. // })
  4726. // test('fragment too many children', () => {
  4727. // const { container } = mountWithHydration(
  4728. // `<div data-allow-mismatch="children"><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>`,
  4729. // () => h('div', [[h('div', 'foo')], h('div', 'baz')]),
  4730. // )
  4731. // expect(container.innerHTML).toBe(
  4732. // '<div data-allow-mismatch="children"><!--[--><div>foo</div><!--]--><div>baz</div></div>',
  4733. // )
  4734. // // fragment ends early and attempts to hydrate the extra <div>bar</div>
  4735. // // as 2nd fragment child.
  4736. // expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
  4737. // // excessive children removal
  4738. // expect(`Hydration children mismatch`).not.toHaveBeenWarned()
  4739. // })
  4740. // test('comment mismatch (element)', () => {
  4741. // const { container } = mountWithHydration(
  4742. // `<div data-allow-mismatch="children"><span></span></div>`,
  4743. // () => h('div', [createCommentVNode('hi')]),
  4744. // )
  4745. // expect(container.innerHTML).toBe(
  4746. // '<div data-allow-mismatch="children"><!--hi--></div>',
  4747. // )
  4748. // expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  4749. // })
  4750. // test('comment mismatch (text)', () => {
  4751. // const { container } = mountWithHydration(
  4752. // `<div data-allow-mismatch="children">foobar</div>`,
  4753. // () => h('div', [createCommentVNode('hi')]),
  4754. // )
  4755. // expect(container.innerHTML).toBe(
  4756. // '<div data-allow-mismatch="children"><!--hi--></div>',
  4757. // )
  4758. // expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  4759. // })
  4760. test('class mismatch', async () => {
  4761. await mountWithHydration(
  4762. `<div class="foo bar" data-allow-mismatch="class"></div>`,
  4763. `<div :class="data"></div>`,
  4764. ref('foo'),
  4765. )
  4766. expect(`Hydration class mismatch`).not.toHaveBeenWarned()
  4767. })
  4768. test('style mismatch', async () => {
  4769. await mountWithHydration(
  4770. `<div style="color:red;" data-allow-mismatch="style"></div>`,
  4771. `<div :style="data"></div>`,
  4772. ref({ color: 'green' }),
  4773. )
  4774. expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  4775. })
  4776. test('attr mismatch', async () => {
  4777. await mountWithHydration(
  4778. `<div data-allow-mismatch="attribute"></div>`,
  4779. `<div :id="data"></div>`,
  4780. ref('foo'),
  4781. )
  4782. await mountWithHydration(
  4783. `<div id="bar" data-allow-mismatch="attribute"></div>`,
  4784. `<div :id="data"></div>`,
  4785. ref('foo'),
  4786. )
  4787. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  4788. })
  4789. })
  4790. describe('VDOM interop', () => {
  4791. // Previous tests (e.g. createVaporSSRApp) leave isHydratingEnabled = true.
  4792. beforeEach(() => {
  4793. setIsHydratingEnabled(false)
  4794. })
  4795. test('hydrate VDOM -> Vapor component should invoke vnode and directive mount hooks in VDOM order', async () => {
  4796. const calls: string[] = []
  4797. const dir = {
  4798. created: vi.fn(() => calls.push('directive created')),
  4799. beforeMount: vi.fn(() => calls.push('directive beforeMount')),
  4800. mounted: vi.fn(() => calls.push('directive mounted')),
  4801. }
  4802. const serverVaporChild = compile(
  4803. `<template><div>child</div></template>`,
  4804. ref({}),
  4805. {},
  4806. {
  4807. vapor: true,
  4808. ssr: true,
  4809. },
  4810. )
  4811. const clientVaporChild = compile(
  4812. `<template><div>child</div></template>`,
  4813. ref({}),
  4814. {},
  4815. {
  4816. vapor: true,
  4817. ssr: false,
  4818. },
  4819. )
  4820. const ServerApp = defineComponent({
  4821. setup() {
  4822. return () =>
  4823. withDirectives(
  4824. h(serverVaporChild as any, {
  4825. onVnodeBeforeMount: () => calls.push('vnode beforeMount'),
  4826. onVnodeMounted: () => calls.push('vnode mounted'),
  4827. }),
  4828. [[dir]],
  4829. )
  4830. },
  4831. })
  4832. const ClientApp = defineComponent({
  4833. setup() {
  4834. return () =>
  4835. withDirectives(
  4836. h(clientVaporChild as any, {
  4837. onVnodeBeforeMount: () => calls.push('vnode beforeMount'),
  4838. onVnodeMounted: () => calls.push('vnode mounted'),
  4839. }),
  4840. [[dir]],
  4841. )
  4842. },
  4843. })
  4844. const html = await VueServerRenderer.renderToString(createSSRApp(ServerApp))
  4845. const container = document.createElement('div')
  4846. document.body.appendChild(container)
  4847. container.innerHTML = html
  4848. const app = createSSRApp(ClientApp)
  4849. app.use(runtimeVapor.vaporInteropPlugin)
  4850. app.mount(container)
  4851. await nextTick()
  4852. expect(calls).toEqual([
  4853. 'vnode beforeMount',
  4854. 'directive created',
  4855. 'directive beforeMount',
  4856. 'directive mounted',
  4857. 'vnode mounted',
  4858. ])
  4859. })
  4860. test('basic render vapor component', async () => {
  4861. const data = ref(true)
  4862. const { container } = await testWithVDOMApp(
  4863. `<script setup>const data = _data; const components = _components;</script>
  4864. <template>
  4865. <components.VaporChild/>
  4866. </template>`,
  4867. {
  4868. VaporChild: {
  4869. code: `<template>{{ data }}</template>`,
  4870. vapor: true,
  4871. },
  4872. },
  4873. data,
  4874. )
  4875. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"true"`)
  4876. data.value = false
  4877. await nextTick()
  4878. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"false"`)
  4879. })
  4880. test('nested components (VDOM -> Vapor -> VDOM)', async () => {
  4881. const data = ref(true)
  4882. const { container } = await testWithVDOMApp(
  4883. `<script setup>const data = _data; const components = _components;</script>
  4884. <template>
  4885. <components.VaporChild/>
  4886. </template>`,
  4887. {
  4888. VaporChild: {
  4889. code: `<template><components.VdomChild/></template>`,
  4890. vapor: true,
  4891. },
  4892. VdomChild: {
  4893. code: `<script setup>const data = _data;</script>
  4894. <template>{{ data }}</template>`,
  4895. vapor: false,
  4896. },
  4897. },
  4898. data,
  4899. )
  4900. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"true"`)
  4901. data.value = false
  4902. await nextTick()
  4903. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"false"`)
  4904. })
  4905. test('nested components (VDOM -> Vapor(multi-root) -> VDOM)', async () => {
  4906. const data = ref('foo')
  4907. const { container } = await testWithVDOMApp(
  4908. `<script setup>const data = _data; const components = _components;</script>
  4909. <template>
  4910. <components.VaporChild/>
  4911. </template>`,
  4912. {
  4913. // Vapor component with multiple root nodes, VDOM child as first element
  4914. // This ensures hydration starts at <!--[--> and tests skipFragmentAnchor
  4915. VaporChild: {
  4916. code: `<template><components.VdomChild/><div>second</div></template>`,
  4917. vapor: true,
  4918. },
  4919. VdomChild: {
  4920. code: `<script setup>const data = _data;</script>
  4921. <template><span>{{ data }}</span></template>`,
  4922. vapor: false,
  4923. },
  4924. },
  4925. data,
  4926. )
  4927. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  4928. `
  4929. "
  4930. <!--[--><span>foo</span><div>second</div><!--]-->
  4931. "
  4932. `,
  4933. )
  4934. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  4935. data.value = 'bar'
  4936. await nextTick()
  4937. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  4938. `
  4939. "
  4940. <!--[--><span>bar</span><div>second</div><!--]-->
  4941. "
  4942. `,
  4943. )
  4944. })
  4945. test('nested components (VDOM -> Vapor(multi-root) -> VDOM) with preceding sibling', async () => {
  4946. const data = ref('foo')
  4947. const { container } = await testWithVDOMApp(
  4948. `<script setup>const data = _data; const components = _components;</script>
  4949. <template>
  4950. <p>before</p>
  4951. <components.VaporChild/>
  4952. </template>`,
  4953. {
  4954. VaporChild: {
  4955. code: `<template><components.VdomChild/><div>second</div></template>`,
  4956. vapor: true,
  4957. },
  4958. VdomChild: {
  4959. code: `<script setup>const data = _data;</script>
  4960. <template><span>{{ data }}</span></template>`,
  4961. vapor: false,
  4962. },
  4963. },
  4964. data,
  4965. )
  4966. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  4967. `
  4968. "
  4969. <!--[--><p>before</p>
  4970. <!--[--><span>foo</span><div>second</div><!--]-->
  4971. <!--]-->
  4972. "
  4973. `,
  4974. )
  4975. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  4976. data.value = 'bar'
  4977. await nextTick()
  4978. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  4979. `
  4980. "
  4981. <!--[--><p>before</p>
  4982. <!--[--><span>bar</span><div>second</div><!--]-->
  4983. <!--]-->
  4984. "
  4985. `,
  4986. )
  4987. })
  4988. test('nested components (VDOM -> Vapor) should not duplicate', async () => {
  4989. const { container } = await testWithVDOMApp(
  4990. `<script setup>const components = _components;</script>
  4991. <template>
  4992. <components.VaporChild/>
  4993. </template>`,
  4994. {
  4995. VaporChild: {
  4996. code: `<script vapor>
  4997. import { ref } from 'vue'
  4998. const show = ref(true)
  4999. </script>
  5000. <template>
  5001. <template v-if="show">
  5002. <div>1</div>
  5003. <div>2</div>
  5004. </template>
  5005. </template>`,
  5006. vapor: true,
  5007. },
  5008. },
  5009. )
  5010. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5011. `
  5012. "
  5013. <!--[--><div>1</div><div>2</div><!--]-->
  5014. "
  5015. `,
  5016. )
  5017. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  5018. })
  5019. test('nested components (VDOM -> Vapor -> VDOM (with slot fallback))', async () => {
  5020. const data = ref(true)
  5021. const { container } = await testWithVDOMApp(
  5022. `<script setup>const data = _data; const components = _components;</script>
  5023. <template>
  5024. <components.VaporChild/>
  5025. </template>`,
  5026. {
  5027. VaporChild: {
  5028. code: `<template><components.VdomChild/></template>`,
  5029. vapor: true,
  5030. },
  5031. VdomChild: {
  5032. code: `<script setup>const data = _data;</script>
  5033. <template><slot><span>{{data}}</span></slot></template>`,
  5034. vapor: false,
  5035. },
  5036. },
  5037. data,
  5038. )
  5039. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5040. `
  5041. "
  5042. <!--[--><span>true</span><!--]-->
  5043. "
  5044. `,
  5045. )
  5046. data.value = false
  5047. await nextTick()
  5048. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5049. `
  5050. "
  5051. <!--[--><span>false</span><!--]-->
  5052. "
  5053. `,
  5054. )
  5055. })
  5056. test('nested components (VDOM -> Vapor(with slot content) -> VDOM)', async () => {
  5057. const data = ref(true)
  5058. const { container } = await testWithVDOMApp(
  5059. `<script setup>const data = _data; const components = _components;</script>
  5060. <template>
  5061. <components.VaporChild/>
  5062. </template>`,
  5063. {
  5064. VaporChild: {
  5065. code: `<template>
  5066. <components.VdomChild>
  5067. <template #default>
  5068. <span>{{data}} vapor fallback</span>
  5069. </template>
  5070. </components.VdomChild>
  5071. </template>`,
  5072. vapor: true,
  5073. },
  5074. VdomChild: {
  5075. code: `<script setup>const data = _data;</script>
  5076. <template><slot><span>vdom fallback</span></slot></template>`,
  5077. vapor: false,
  5078. },
  5079. },
  5080. data,
  5081. )
  5082. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5083. `
  5084. "
  5085. <!--[--><span>true vapor fallback</span><!--]-->
  5086. "
  5087. `,
  5088. )
  5089. data.value = false
  5090. await nextTick()
  5091. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5092. `
  5093. "
  5094. <!--[--><span>false vapor fallback</span><!--]-->
  5095. "
  5096. `,
  5097. )
  5098. })
  5099. test('nested components (VDOM -> Vapor(with slot content) -> Vapor)', async () => {
  5100. const data = ref(true)
  5101. const { container } = await testWithVDOMApp(
  5102. `<script setup>const data = _data; const components = _components;</script>
  5103. <template>
  5104. <components.VaporChild/>
  5105. </template>`,
  5106. {
  5107. VaporChild: {
  5108. code: `<template>
  5109. <components.VaporChild2>
  5110. <template #default>
  5111. <span>{{data}} vapor fallback</span>
  5112. </template>
  5113. </components.VaporChild2>
  5114. </template>`,
  5115. vapor: true,
  5116. },
  5117. VaporChild2: {
  5118. code: `<template><slot><span>vapor fallback2</span></slot></template>`,
  5119. vapor: true,
  5120. },
  5121. },
  5122. data,
  5123. )
  5124. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5125. `
  5126. "
  5127. <!--[--><span>true vapor fallback</span><!--]-->
  5128. "
  5129. `,
  5130. )
  5131. data.value = false
  5132. await nextTick()
  5133. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5134. `
  5135. "
  5136. <!--[--><span>false vapor fallback</span><!--]-->
  5137. "
  5138. `,
  5139. )
  5140. })
  5141. test('vapor slot render vdom component', async () => {
  5142. const data = ref(true)
  5143. const { container } = await testWithVDOMApp(
  5144. `<script setup>const data = _data; const components = _components;</script>
  5145. <template>
  5146. <components.VaporChild>
  5147. <components.VdomChild/>
  5148. </components.VaporChild>
  5149. </template>`,
  5150. {
  5151. VaporChild: {
  5152. code: `<template><div><slot/></div></template>`,
  5153. vapor: true,
  5154. },
  5155. VdomChild: {
  5156. code: `<script setup>const data = _data;</script>
  5157. <template>{{ data }}</template>`,
  5158. vapor: false,
  5159. },
  5160. },
  5161. data,
  5162. )
  5163. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5164. `
  5165. "<div>
  5166. <!--[-->true<!--]-->
  5167. </div>"
  5168. `,
  5169. )
  5170. data.value = false
  5171. await nextTick()
  5172. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5173. `
  5174. "<div>
  5175. <!--[-->false<!--]-->
  5176. </div>"
  5177. `,
  5178. )
  5179. })
  5180. test('vapor slot render vdom component (multi-root slot content)', async () => {
  5181. const data = ref('foo')
  5182. const { container } = await testWithVaporApp(
  5183. `<script setup>const data = _data; const components = _components;</script>
  5184. <template>
  5185. <components.VaporChild>
  5186. <components.VdomChild/>
  5187. <div>vapor content</div>
  5188. </components.VaporChild>
  5189. </template>`,
  5190. {
  5191. VaporChild: {
  5192. code: `<template><div><slot/></div></template>`,
  5193. vapor: true,
  5194. },
  5195. VdomChild: {
  5196. code: `<script setup>const data = _data;</script>
  5197. <template><span>{{ data }}</span></template>`,
  5198. vapor: false,
  5199. },
  5200. },
  5201. data,
  5202. )
  5203. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5204. `
  5205. "<div>
  5206. <!--[--><span>foo</span><div>vapor content</div><!--]-->
  5207. </div>"
  5208. `,
  5209. )
  5210. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  5211. data.value = 'bar'
  5212. await nextTick()
  5213. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5214. `
  5215. "<div>
  5216. <!--[--><span>bar</span><div>vapor content</div><!--]-->
  5217. </div>"
  5218. `,
  5219. )
  5220. })
  5221. test('vapor slot render vdom component (render function)', async () => {
  5222. const data = ref(true)
  5223. const { container } = await testWithVaporApp(
  5224. `<script setup>
  5225. import { h } from 'vue'
  5226. const data = _data; const components = _components;
  5227. const VdomChild = {
  5228. setup() {
  5229. return () => h('div', null, [h('div', [String(data.value)])])
  5230. }
  5231. }
  5232. </script>
  5233. <template>
  5234. <components.VaporChild>
  5235. <VdomChild/>
  5236. </components.VaporChild>
  5237. </template>`,
  5238. {
  5239. VaporChild: {
  5240. code: `<template><div><slot/></div></template>`,
  5241. vapor: true,
  5242. },
  5243. },
  5244. data,
  5245. )
  5246. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5247. `
  5248. "<div>
  5249. <!--[--><div><div>true</div></div><!--]-->
  5250. </div>"
  5251. `,
  5252. )
  5253. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  5254. data.value = false
  5255. await nextTick()
  5256. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5257. `
  5258. "<div>
  5259. <!--[--><div><div>false</div></div><!--]-->
  5260. </div>"
  5261. `,
  5262. )
  5263. })
  5264. test('hydrate VNode rendered via createDynamicComponent', async () => {
  5265. const data = ref('foo')
  5266. const { container } = await testWithVaporApp(
  5267. `<script setup>
  5268. import { h } from 'vue'
  5269. const data = _data; const components = _components;
  5270. // Simulating RouterView pattern: VDOM component passes VNode through slot
  5271. const RouterView = {
  5272. setup(_, { slots }) {
  5273. return () => {
  5274. const component = h(components.VaporChild)
  5275. return slots.default({ Component: component })
  5276. }
  5277. }
  5278. }
  5279. </script>
  5280. <template>
  5281. <RouterView v-slot="{ Component }">
  5282. <component :is="Component" />
  5283. </RouterView>
  5284. </template>`,
  5285. {
  5286. VaporChild: {
  5287. code: `<template><div>{{ data }}</div></template>`,
  5288. vapor: true,
  5289. },
  5290. },
  5291. data,
  5292. )
  5293. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5294. `
  5295. "
  5296. <!--[--><div>foo</div><!--dynamic-component--><!--]-->
  5297. "
  5298. `,
  5299. )
  5300. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  5301. data.value = 'bar'
  5302. await nextTick()
  5303. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5304. `
  5305. "
  5306. <!--[--><div>bar</div><!--dynamic-component--><!--]-->
  5307. "
  5308. `,
  5309. )
  5310. })
  5311. test('hydrate static VNode chunk rendered via createDynamicComponent', async () => {
  5312. const data = ref('foo')
  5313. const { container } = await testWithVaporApp(
  5314. `<script setup>
  5315. import { createStaticVNode } from 'vue'
  5316. const data = _data
  5317. const StaticChunk = createStaticVNode(
  5318. '<div>first static</div><div>second static</div>',
  5319. 2,
  5320. )
  5321. </script>
  5322. <template>
  5323. <div>
  5324. <component :is="StaticChunk" />
  5325. </div>
  5326. <span>{{ data }}</span>
  5327. </template>`,
  5328. undefined,
  5329. data,
  5330. )
  5331. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5332. `
  5333. "
  5334. <!--[--><div><div>first static</div><div>second static</div><!--dynamic-component--></div><span>foo</span><!--]-->
  5335. "
  5336. `,
  5337. )
  5338. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  5339. data.value = 'bar'
  5340. await nextTick()
  5341. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5342. `
  5343. "
  5344. <!--[--><div><div>first static</div><div>second static</div><!--dynamic-component--></div><span>bar</span><!--]-->
  5345. "
  5346. `,
  5347. )
  5348. })
  5349. test('hydrate Fragment VNode rendered via createDynamicComponent', async () => {
  5350. const data = ref('foo')
  5351. const { container } = await testWithVaporApp(
  5352. `<script setup>
  5353. import { Fragment, h } from 'vue'
  5354. const data = _data
  5355. const FragmentChunk = h(Fragment, null, [
  5356. h('div', null, 'first fragment'),
  5357. h('div', null, 'second fragment'),
  5358. ])
  5359. </script>
  5360. <template>
  5361. <div>
  5362. <component :is="FragmentChunk" />
  5363. </div>
  5364. <span>{{ data }}</span>
  5365. </template>`,
  5366. undefined,
  5367. data,
  5368. )
  5369. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5370. `
  5371. "
  5372. <!--[--><div>
  5373. <!--[--><div>first fragment</div><div>second fragment</div><!--]-->
  5374. <!--dynamic-component--></div><span>foo</span><!--]-->
  5375. "
  5376. `,
  5377. )
  5378. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  5379. data.value = 'bar'
  5380. await nextTick()
  5381. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5382. `
  5383. "
  5384. <!--[--><div>
  5385. <!--[--><div>first fragment</div><div>second fragment</div><!--]-->
  5386. <!--dynamic-component--></div><span>bar</span><!--]-->
  5387. "
  5388. `,
  5389. )
  5390. })
  5391. test('hydrate vapor slot in vdom component with sibling nodes', async () => {
  5392. const msg = ref('Hello World!')
  5393. const { container } = await testWithVaporApp(
  5394. `<script setup vapor>
  5395. const msg = _data
  5396. const components = _components
  5397. </script>
  5398. <template>
  5399. <components.Comp>
  5400. <h1>{{ msg }}</h1>
  5401. </components.Comp>
  5402. <h1>{{ msg }}</h1>
  5403. </template>`,
  5404. {
  5405. Comp: {
  5406. code: `
  5407. <template>
  5408. <div>
  5409. <slot />
  5410. </div>
  5411. </template>`,
  5412. vapor: false,
  5413. },
  5414. },
  5415. msg,
  5416. )
  5417. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5418. `
  5419. "
  5420. <!--[--><div>
  5421. <!--[--><h1>Hello World!</h1><!--]-->
  5422. </div><h1>Hello World!</h1><!--]-->
  5423. "
  5424. `,
  5425. )
  5426. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  5427. msg.value = 'Hi Vapor'
  5428. await nextTick()
  5429. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5430. `
  5431. "
  5432. <!--[--><div>
  5433. <!--[--><h1>Hi Vapor</h1><!--]-->
  5434. </div><h1>Hi Vapor</h1><!--]-->
  5435. "
  5436. `,
  5437. )
  5438. })
  5439. test('hydrate dynamic vapor slot re-mount should stop stale effects from previous slot function', async () => {
  5440. const staleState = reactive({ id: 0, text: 'zero' })
  5441. const activeState = reactive({ id: 1, text: 'one' })
  5442. const nextState = reactive({ id: 2, text: 'two' })
  5443. const data = reactive({
  5444. items: [staleState, activeState],
  5445. track: vi.fn((_: number, text: string) => text),
  5446. })
  5447. const { container } = await testWithVaporApp(
  5448. `<script setup vapor>
  5449. const data = _data
  5450. const components = _components
  5451. </script>
  5452. <template>
  5453. <components.Comp>
  5454. <template v-for="item in data.items" #default>
  5455. <span>{{ data.track(item.id, item.text) }}</span>
  5456. </template>
  5457. </components.Comp>
  5458. </template>`,
  5459. {
  5460. Comp: {
  5461. code: `
  5462. <template>
  5463. <div>
  5464. <slot />
  5465. </div>
  5466. </template>`,
  5467. vapor: false,
  5468. },
  5469. },
  5470. data,
  5471. )
  5472. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  5473. "<div>
  5474. <!--[--><span>one</span><!--]-->
  5475. </div>"
  5476. `)
  5477. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  5478. data.items.push(nextState)
  5479. await nextTick()
  5480. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  5481. "<div>
  5482. <!--[--><span>two</span><!--]-->
  5483. </div>"
  5484. `)
  5485. data.track.mockClear()
  5486. activeState.text = 'stale-one'
  5487. await nextTick()
  5488. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  5489. "<div>
  5490. <!--[--><span>two</span><!--]-->
  5491. </div>"
  5492. `)
  5493. expect(data.track).not.toHaveBeenCalled()
  5494. })
  5495. test('hydrate multi-root VNode component via createDynamicComponent and switch branch', async () => {
  5496. const data = ref({
  5497. showMulti: true,
  5498. tail: 'tail',
  5499. })
  5500. const { container } = await testWithVaporApp(
  5501. `<script setup>
  5502. import { computed, h } from 'vue'
  5503. const data = _data
  5504. const components = _components
  5505. const vnode = computed(() =>
  5506. data.value.showMulti
  5507. ? h(components.VdomMultiRoot)
  5508. : h('p', null, 'fallback')
  5509. )
  5510. </script>
  5511. <template>
  5512. <component :is="vnode" />
  5513. <span>{{ data.tail }}</span>
  5514. </template>`,
  5515. {
  5516. VdomMultiRoot: {
  5517. code: `<template><div>first</div><div>second</div></template>`,
  5518. vapor: false,
  5519. },
  5520. },
  5521. data,
  5522. )
  5523. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5524. `
  5525. "
  5526. <!--[-->
  5527. <!--[--><!--dynamic-component--><div>first</div><div>second</div><!--]-->
  5528. <span>tail</span><!--]-->
  5529. "
  5530. `,
  5531. )
  5532. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  5533. data.value.showMulti = false
  5534. await nextTick()
  5535. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5536. `
  5537. "
  5538. <!--[-->
  5539. <!--[--><p>fallback</p><!--dynamic-component--><!--]-->
  5540. <span>tail</span><!--]-->
  5541. "
  5542. `,
  5543. )
  5544. data.value.showMulti = true
  5545. await nextTick()
  5546. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5547. `
  5548. "
  5549. <!--[-->
  5550. <!--[--><div>first</div><div>second</div><!--dynamic-component--><!--]-->
  5551. <span>tail</span><!--]-->
  5552. "
  5553. `,
  5554. )
  5555. data.value.tail = 'tail-updated'
  5556. await nextTick()
  5557. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  5558. "
  5559. <!--[-->
  5560. <!--[--><div>first</div><div>second</div><!--dynamic-component--><!--]-->
  5561. <span>tail-updated</span><!--]-->
  5562. "
  5563. `)
  5564. })
  5565. test('hydrate vapor slot in vdom component with empty slot and sibling nodes', async () => {
  5566. const msg = ref('Hello World!')
  5567. const { container } = await testWithVaporApp(
  5568. `<script setup vapor>
  5569. const msg = _data
  5570. const components = _components
  5571. </script>
  5572. <template>
  5573. <components.Comp />
  5574. <h1>{{ msg }}</h1>
  5575. </template>`,
  5576. {
  5577. Comp: {
  5578. code: `
  5579. <template>
  5580. <div>
  5581. <slot />
  5582. </div>
  5583. </template>`,
  5584. vapor: false,
  5585. },
  5586. },
  5587. msg,
  5588. )
  5589. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5590. `
  5591. "
  5592. <!--[--><div>
  5593. <!--[--><!--]-->
  5594. </div><h1>Hello World!</h1><!--]-->
  5595. "
  5596. `,
  5597. )
  5598. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  5599. msg.value = 'Hi Vapor'
  5600. await nextTick()
  5601. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5602. `
  5603. "
  5604. <!--[--><div>
  5605. <!--[--><!--]-->
  5606. </div><h1>Hi Vapor</h1><!--]-->
  5607. "
  5608. `,
  5609. )
  5610. })
  5611. test('hydrate static/fragment VNode via createDynamicComponent and switch type', async () => {
  5612. const data = ref({
  5613. useStatic: true,
  5614. tail: 'tail',
  5615. })
  5616. const { container } = await testWithVaporApp(
  5617. `<script setup>
  5618. import { Fragment, computed, createStaticVNode, h } from 'vue'
  5619. const data = _data
  5620. const vnode = computed(() =>
  5621. data.value.useStatic
  5622. ? createStaticVNode(
  5623. '<div>first static</div><div>second static</div>',
  5624. 2,
  5625. )
  5626. : h(Fragment, null, [
  5627. h('div', null, 'first fragment'),
  5628. h('div', null, 'second fragment'),
  5629. ])
  5630. )
  5631. </script>
  5632. <template>
  5633. <component :is="vnode" />
  5634. <span>{{ data.tail }}</span>
  5635. </template>`,
  5636. undefined,
  5637. data,
  5638. )
  5639. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5640. `
  5641. "
  5642. <!--[--><div>first static</div><div>second static</div><!--dynamic-component--><span>tail</span><!--]-->
  5643. "
  5644. `,
  5645. )
  5646. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  5647. data.value.useStatic = false
  5648. await nextTick()
  5649. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5650. `
  5651. "
  5652. <!--[--><div>first fragment</div><div>second fragment</div><!--dynamic-component--><span>tail</span><!--]-->
  5653. "
  5654. `,
  5655. )
  5656. data.value.useStatic = true
  5657. await nextTick()
  5658. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5659. `
  5660. "
  5661. <!--[--><div>first static</div><div>second static</div><!--dynamic-component--><span>tail</span><!--]-->
  5662. "
  5663. `,
  5664. )
  5665. data.value.tail = 'tail-updated'
  5666. await nextTick()
  5667. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  5668. "
  5669. <!--[--><div>first static</div><div>second static</div><!--dynamic-component--><span>tail-updated</span><!--]-->
  5670. "
  5671. `)
  5672. })
  5673. test('hydrate Teleport VNode via createDynamicComponent and switch branch', async () => {
  5674. const data = ref({
  5675. showTeleport: true,
  5676. tail: 'tail',
  5677. })
  5678. const { container } = await testWithVaporApp(
  5679. `<script setup>
  5680. import { Teleport, computed, h } from 'vue'
  5681. const data = _data
  5682. const vnode = computed(() =>
  5683. data.value.showTeleport
  5684. ? h(Teleport, { to: '#target', disabled: true }, [
  5685. h('div', null, 'teleported'),
  5686. ])
  5687. : h('p', null, 'fallback')
  5688. )
  5689. </script>
  5690. <template>
  5691. <component :is="vnode" />
  5692. <span>{{ data.tail }}</span>
  5693. </template>`,
  5694. undefined,
  5695. data,
  5696. )
  5697. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  5698. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  5699. "
  5700. <!--[-->
  5701. <!--teleport start-->
  5702. <div>teleported</div>
  5703. <!--teleport end-->
  5704. <!--dynamic-component--><span>tail</span><!--]-->
  5705. "
  5706. `)
  5707. data.value.showTeleport = false
  5708. await nextTick()
  5709. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  5710. "
  5711. <!--[--><p>fallback</p><!--dynamic-component--><span>tail</span><!--]-->
  5712. "
  5713. `)
  5714. data.value.showTeleport = true
  5715. await nextTick()
  5716. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  5717. "
  5718. <!--[-->
  5719. <!--teleport start-->
  5720. <div>teleported</div>
  5721. <!--teleport end-->
  5722. <!--dynamic-component--><span>tail</span><!--]-->
  5723. "
  5724. `)
  5725. data.value.tail = 'tail-updated'
  5726. await nextTick()
  5727. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  5728. "
  5729. <!--[-->
  5730. <!--teleport start-->
  5731. <div>teleported</div>
  5732. <!--teleport end-->
  5733. <!--dynamic-component--><span>tail-updated</span><!--]-->
  5734. "
  5735. `)
  5736. })
  5737. test('hydrate enabled Teleport VNode via createDynamicComponent and switch branch', async () => {
  5738. const data = ref({
  5739. showTeleport: true,
  5740. tail: 'tail',
  5741. })
  5742. const code = `<script setup>
  5743. import { Teleport, computed, h } from 'vue'
  5744. const data = _data
  5745. const vnode = computed(() =>
  5746. data.value.showTeleport
  5747. ? h(Teleport, { to: '#target' }, [h('div', null, 'teleported')])
  5748. : h('p', null, 'fallback')
  5749. )
  5750. </script>
  5751. <template>
  5752. <component :is="vnode" />
  5753. <span>{{ data.tail }}</span>
  5754. </template>`
  5755. const serverComp = compile(code, data, {}, { vapor: true, ssr: true })
  5756. const ssrCtx: Record<string, any> = {}
  5757. const html = await VueServerRenderer.renderToString(
  5758. runtimeDom.createSSRApp(serverComp),
  5759. ssrCtx,
  5760. )
  5761. const target = document.createElement('div')
  5762. target.id = 'target'
  5763. target.innerHTML = ssrCtx.teleports['#target']
  5764. document.body.appendChild(target)
  5765. const container = document.createElement('div')
  5766. container.innerHTML = html
  5767. document.body.appendChild(container)
  5768. const clientComp = compile(code, data, {}, { vapor: true, ssr: false })
  5769. const app = createVaporSSRApp(clientComp)
  5770. app.use(runtimeVapor.vaporInteropPlugin)
  5771. app.mount(container)
  5772. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  5773. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5774. `
  5775. "
  5776. <!--[-->
  5777. <!--teleport start-->
  5778. <!--teleport end-->
  5779. <!--dynamic-component--><span>tail</span><!--]-->
  5780. "
  5781. `,
  5782. )
  5783. expect(formatHtml(target.innerHTML)).toMatchInlineSnapshot(
  5784. `"<!--teleport start anchor--><div>teleported</div><!--teleport anchor-->"`,
  5785. )
  5786. data.value.showTeleport = false
  5787. await nextTick()
  5788. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5789. `
  5790. "
  5791. <!--[--><p>fallback</p><!--dynamic-component--><span>tail</span><!--]-->
  5792. "
  5793. `,
  5794. )
  5795. expect(formatHtml(target.innerHTML)).toMatchInlineSnapshot(`""`)
  5796. data.value.showTeleport = true
  5797. await nextTick()
  5798. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5799. `
  5800. "
  5801. <!--[-->
  5802. <!--teleport start-->
  5803. <!--teleport end-->
  5804. <!--dynamic-component--><span>tail</span><!--]-->
  5805. "
  5806. `,
  5807. )
  5808. expect(formatHtml(target.innerHTML)).toMatchInlineSnapshot(
  5809. `"<div>teleported</div>"`,
  5810. )
  5811. data.value.tail = 'tail-updated'
  5812. await nextTick()
  5813. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  5814. "
  5815. <!--[-->
  5816. <!--teleport start-->
  5817. <!--teleport end-->
  5818. <!--dynamic-component--><span>tail-updated</span><!--]-->
  5819. "
  5820. `)
  5821. })
  5822. test('hydrate Suspense VNode via createDynamicComponent and switch branch', async () => {
  5823. const data = ref({
  5824. showSuspense: true,
  5825. msg: 'foo',
  5826. tail: 'tail',
  5827. })
  5828. const { container } = await testWithVaporApp(
  5829. `<script setup>
  5830. import { Suspense, computed, h } from 'vue'
  5831. const data = _data
  5832. const vnode = computed(() =>
  5833. data.value.showSuspense
  5834. ? h(Suspense, null, {
  5835. default: () => h('div', null, data.value.msg),
  5836. fallback: () => h('div', null, 'pending'),
  5837. })
  5838. : h('p', null, 'fallback')
  5839. )
  5840. </script>
  5841. <template>
  5842. <component :is="vnode" />
  5843. <span>{{ data.tail }}</span>
  5844. </template>`,
  5845. undefined,
  5846. data,
  5847. )
  5848. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5849. `
  5850. "
  5851. <!--[--><div>foo</div><!--dynamic-component--><span>tail</span><!--]-->
  5852. "
  5853. `,
  5854. )
  5855. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  5856. data.value.msg = 'bar'
  5857. await nextTick()
  5858. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5859. `
  5860. "
  5861. <!--[--><div>bar</div><!--dynamic-component--><span>tail</span><!--]-->
  5862. "
  5863. `,
  5864. )
  5865. data.value.showSuspense = false
  5866. await nextTick()
  5867. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5868. `
  5869. "
  5870. <!--[--><p>fallback</p><!--dynamic-component--><span>tail</span><!--]-->
  5871. "
  5872. `,
  5873. )
  5874. data.value.showSuspense = true
  5875. await nextTick()
  5876. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  5877. `
  5878. "
  5879. <!--[--><div>bar</div><!--dynamic-component--><span>tail</span><!--]-->
  5880. "
  5881. `,
  5882. )
  5883. data.value.tail = 'tail-updated'
  5884. await nextTick()
  5885. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  5886. "
  5887. <!--[--><div>bar</div><!--dynamic-component--><span>tail-updated</span><!--]-->
  5888. "
  5889. `)
  5890. })
  5891. test('hydrate async Suspense VNode via createDynamicComponent and show fallback', async () => {
  5892. const data = ref({
  5893. showSuspense: true,
  5894. tail: 'tail',
  5895. })
  5896. const appCode = `<script setup>
  5897. import { Suspense, computed, h } from 'vue'
  5898. const data = _data
  5899. const components = _components
  5900. const vnode = computed(() =>
  5901. data.value.showSuspense
  5902. ? h(Suspense, { timeout: 0 }, {
  5903. default: () => h(components.AsyncComp),
  5904. fallback: () => h('div', null, 'pending'),
  5905. })
  5906. : h('p', null, 'fallback')
  5907. )
  5908. </script>
  5909. <template>
  5910. <component :is="vnode" />
  5911. <span>{{ data.tail }}</span>
  5912. </template>`
  5913. const AsyncResolvedComp = {
  5914. render: () => runtimeDom.h('div', null, 'async resolved'),
  5915. }
  5916. let serverResolve: (comp: any) => void
  5917. const ServerAsyncComp = defineAsyncComponent(
  5918. () =>
  5919. new Promise(r => {
  5920. serverResolve = r
  5921. }),
  5922. )
  5923. const SSRApp = compile(
  5924. appCode,
  5925. data,
  5926. { AsyncComp: ServerAsyncComp },
  5927. {
  5928. vapor: true,
  5929. ssr: true,
  5930. },
  5931. )
  5932. const htmlPromise = VueServerRenderer.renderToString(
  5933. runtimeDom.createSSRApp(SSRApp),
  5934. )
  5935. serverResolve!(AsyncResolvedComp)
  5936. const html = await htmlPromise
  5937. let clientResolve: (comp: any) => void
  5938. const ClientAsyncComp = defineAsyncComponent(
  5939. () =>
  5940. new Promise(r => {
  5941. clientResolve = r
  5942. }),
  5943. )
  5944. const App = compile(
  5945. appCode,
  5946. data,
  5947. { AsyncComp: ClientAsyncComp },
  5948. {
  5949. vapor: true,
  5950. ssr: false,
  5951. },
  5952. )
  5953. const container = document.createElement('div')
  5954. container.innerHTML = html
  5955. document.body.appendChild(container)
  5956. const app = createVaporSSRApp(App)
  5957. app.use(runtimeVapor.vaporInteropPlugin)
  5958. app.mount(container)
  5959. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  5960. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  5961. "
  5962. <!--[--><div>async resolved</div><!--dynamic-component--><span>tail</span><!--]-->
  5963. "
  5964. `)
  5965. data.value.showSuspense = false
  5966. await nextTick()
  5967. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  5968. "
  5969. <!--[--><div>async resolved</div><p>fallback</p><!--dynamic-component--><span>tail</span><!--]-->
  5970. "
  5971. `)
  5972. data.value.showSuspense = true
  5973. await nextTick()
  5974. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  5975. "
  5976. <!--[--><div>async resolved</div><div>pending</div><!--dynamic-component--><span>tail</span><!--]-->
  5977. "
  5978. `)
  5979. clientResolve!(AsyncResolvedComp)
  5980. await new Promise(r => setTimeout(r))
  5981. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  5982. "
  5983. <!--[--><div>async resolved</div><div>async resolved</div><!--dynamic-component--><span>tail</span><!--]-->
  5984. "
  5985. `)
  5986. data.value.tail = 'tail-updated'
  5987. await nextTick()
  5988. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  5989. "
  5990. <!--[--><div>async resolved</div><div>async resolved</div><!--dynamic-component--><span>tail-updated</span><!--]-->
  5991. "
  5992. `)
  5993. })
  5994. test('hydrate Suspense VNode via createDynamicComponent under KeepAlive', async () => {
  5995. const data = ref({
  5996. msg: 'foo',
  5997. tail: 'tail',
  5998. })
  5999. const { container } = await testWithVaporApp(
  6000. `<script setup>
  6001. import { KeepAlive, Suspense, computed, h } from 'vue'
  6002. const data = _data
  6003. const vnode = computed(() =>
  6004. h(Suspense, null, {
  6005. default: () => h('div', null, data.value.msg),
  6006. fallback: () => h('div', null, 'pending'),
  6007. })
  6008. )
  6009. </script>
  6010. <template>
  6011. <KeepAlive>
  6012. <component :is="vnode" />
  6013. </KeepAlive>
  6014. <span>{{ data.tail }}</span>
  6015. </template>`,
  6016. undefined,
  6017. data,
  6018. )
  6019. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  6020. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  6021. "
  6022. <!--[--><div>foo</div><!--dynamic-component--><span>tail</span><!--]-->
  6023. "
  6024. `)
  6025. data.value.tail = 'tail-updated'
  6026. await nextTick()
  6027. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  6028. "
  6029. <!--[--><div>foo</div><!--dynamic-component--><span>tail-updated</span><!--]-->
  6030. "
  6031. `)
  6032. })
  6033. test('hydrate Teleport VNode via createDynamicComponent under Transition', async () => {
  6034. const data = ref({
  6035. showTeleport: true,
  6036. tail: 'tail',
  6037. })
  6038. const { container } = await testWithVaporApp(
  6039. `<script setup>
  6040. import { Teleport, Transition, computed, h } from 'vue'
  6041. const data = _data
  6042. const vnode = computed(() =>
  6043. data.value.showTeleport
  6044. ? h(Teleport, { to: '#target', disabled: true }, [
  6045. h('div', null, 'teleported'),
  6046. ])
  6047. : h('p', null, 'fallback')
  6048. )
  6049. </script>
  6050. <template>
  6051. <Transition>
  6052. <component :is="vnode" />
  6053. </Transition>
  6054. <span>{{ data.tail }}</span>
  6055. </template>`,
  6056. undefined,
  6057. data,
  6058. )
  6059. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  6060. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  6061. "
  6062. <!--[-->
  6063. <!--teleport start-->
  6064. <div>teleported</div>
  6065. <!--teleport end-->
  6066. <!--dynamic-component--><span>tail</span><!--]-->
  6067. "
  6068. `)
  6069. data.value.showTeleport = false
  6070. await nextTick()
  6071. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  6072. "
  6073. <!--[--><p class="v-enter-from v-enter-active">fallback</p><!--dynamic-component--><span>tail</span><!--]-->
  6074. "
  6075. `)
  6076. data.value.showTeleport = true
  6077. await nextTick()
  6078. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  6079. "
  6080. <!--[--><p class="v-enter-from v-leave-from v-leave-active">fallback</p>
  6081. <!--teleport start-->
  6082. <div>teleported</div>
  6083. <!--teleport end-->
  6084. <!--dynamic-component--><span>tail</span><!--]-->
  6085. "
  6086. `)
  6087. data.value.tail = 'tail-updated'
  6088. await nextTick()
  6089. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  6090. "
  6091. <!--[--><p class="v-enter-from v-leave-from v-leave-active">fallback</p>
  6092. <!--teleport start-->
  6093. <div>teleported</div>
  6094. <!--teleport end-->
  6095. <!--dynamic-component--><span>tail-updated</span><!--]-->
  6096. "
  6097. `)
  6098. })
  6099. test('hydrate interop dynamic component under KeepAlive', async () => {
  6100. const data = ref({
  6101. show: true,
  6102. tail: 'tail',
  6103. })
  6104. const { container } = await testWithVaporApp(
  6105. `<script setup>
  6106. import { KeepAlive, computed, h } from 'vue'
  6107. const data = _data
  6108. const components = _components
  6109. const vnode = computed(() => h(components.Counter))
  6110. </script>
  6111. <template>
  6112. <KeepAlive>
  6113. <component v-if="data.show" :is="vnode" />
  6114. </KeepAlive>
  6115. <span>{{ data.tail }}</span>
  6116. </template>`,
  6117. {
  6118. Counter: {
  6119. code: `<script setup>
  6120. import { ref } from 'vue'
  6121. const count = ref(0)
  6122. </script>
  6123. <template><button @click="count++">{{ count }}</button></template>`,
  6124. vapor: false,
  6125. },
  6126. },
  6127. data,
  6128. )
  6129. const getButton = () =>
  6130. container.querySelector('button') as HTMLButtonElement
  6131. expect(getButton().textContent).toBe('0')
  6132. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  6133. triggerEvent('click', getButton())
  6134. await nextTick()
  6135. expect(getButton().textContent).toBe('1')
  6136. data.value.show = false
  6137. await nextTick()
  6138. expect(container.querySelector('button')).toBeNull()
  6139. data.value.show = true
  6140. await nextTick()
  6141. expect(getButton().textContent).toBe('1')
  6142. data.value.tail = 'tail-updated'
  6143. await nextTick()
  6144. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  6145. "
  6146. <!--[--><button>1</button><!--dynamic-component--><!--if--><span>tail-updated</span><!--]-->
  6147. "
  6148. `)
  6149. })
  6150. test('hydrate VDOM slot content', async () => {
  6151. const data = ref('foo')
  6152. const { container } = await testWithVaporApp(
  6153. `<script setup>
  6154. const data = _data; const components = _components;
  6155. </script>
  6156. <template>
  6157. <components.VdomWrapper>
  6158. <div>{{ data }}</div>
  6159. </components.VdomWrapper>
  6160. </template>`,
  6161. {
  6162. VdomWrapper: {
  6163. code: `<script setup>const data = _data;</script>
  6164. <template><slot /></template>`,
  6165. vapor: false,
  6166. },
  6167. },
  6168. data,
  6169. )
  6170. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6171. `
  6172. "
  6173. <!--[--><div>foo</div><!--]-->
  6174. "
  6175. `,
  6176. )
  6177. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  6178. data.value = 'bar'
  6179. await nextTick()
  6180. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6181. `
  6182. "
  6183. <!--[--><div>bar</div><!--]-->
  6184. "
  6185. `,
  6186. )
  6187. })
  6188. test('hydrate VDOM slot content should unmount hydrated slot child before first insert', async () => {
  6189. const data = ref({
  6190. unmounted: vi.fn(),
  6191. })
  6192. const appCode = `<script setup>
  6193. const components = _components
  6194. </script>
  6195. <template>
  6196. <components.VaporChild>
  6197. <components.SlotChild />
  6198. </components.VaporChild>
  6199. </template>`
  6200. const ssrComponents = {
  6201. VaporChild: compile(
  6202. `<template><slot /></template>`,
  6203. data,
  6204. {},
  6205. {
  6206. vapor: true,
  6207. ssr: true,
  6208. },
  6209. ),
  6210. SlotChild: compile(
  6211. `<script setup>
  6212. import { onUnmounted } from 'vue'
  6213. const data = _data
  6214. onUnmounted(() => data.value.unmounted())
  6215. </script>
  6216. <template><div>slot child</div></template>`,
  6217. data,
  6218. {},
  6219. {
  6220. vapor: false,
  6221. ssr: true,
  6222. },
  6223. ),
  6224. }
  6225. const clientComponents = {
  6226. VaporChild: compile(
  6227. `<template><slot /></template>`,
  6228. data,
  6229. {},
  6230. {
  6231. vapor: true,
  6232. ssr: false,
  6233. },
  6234. ),
  6235. SlotChild: compile(
  6236. `<script setup>
  6237. import { onUnmounted } from 'vue'
  6238. const data = _data
  6239. onUnmounted(() => data.value.unmounted())
  6240. </script>
  6241. <template><div>slot child</div></template>`,
  6242. data,
  6243. {},
  6244. {
  6245. vapor: false,
  6246. ssr: false,
  6247. },
  6248. ),
  6249. }
  6250. const serverComp = compile(appCode, data, ssrComponents, {
  6251. vapor: false,
  6252. ssr: true,
  6253. })
  6254. const html = await VueServerRenderer.renderToString(
  6255. runtimeDom.createSSRApp(serverComp),
  6256. )
  6257. const container = document.createElement('div')
  6258. document.body.appendChild(container)
  6259. container.innerHTML = html
  6260. const clientComp = compile(appCode, data, clientComponents, {
  6261. vapor: false,
  6262. ssr: false,
  6263. })
  6264. const app = runtimeDom.createSSRApp(clientComp)
  6265. app.use(runtimeVapor.vaporInteropPlugin)
  6266. app.mount(container)
  6267. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  6268. app.unmount()
  6269. await nextTick()
  6270. expect(data.value.unmounted).toHaveBeenCalledTimes(1)
  6271. })
  6272. test('hydrate VDOM slot fallback', async () => {
  6273. const data = ref('foo')
  6274. const { container } = await testWithVaporApp(
  6275. `<script setup>
  6276. const data = _data; const components = _components;
  6277. </script>
  6278. <template>
  6279. <components.VdomWrapper />
  6280. </template>`,
  6281. {
  6282. VdomWrapper: {
  6283. code: `<script setup>const data = _data;</script>
  6284. <template><slot><div>{{ data }}</div></slot></template>`,
  6285. vapor: false,
  6286. },
  6287. },
  6288. data,
  6289. )
  6290. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6291. `
  6292. "
  6293. <!--[--><div>foo</div><!--]-->
  6294. "
  6295. `,
  6296. )
  6297. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  6298. data.value = 'bar'
  6299. await nextTick()
  6300. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6301. `
  6302. "
  6303. <!--[--><div>bar</div><!--]-->
  6304. "
  6305. `,
  6306. )
  6307. })
  6308. test('hydrate VDOM slot content can mount after hydrating as empty', async () => {
  6309. const data = reactive({
  6310. show: false,
  6311. text: 'foo',
  6312. })
  6313. const { container } = await testWithVDOMApp(
  6314. `<script setup>
  6315. const data = _data
  6316. const components = _components
  6317. </script>
  6318. <template>
  6319. <components.VaporChild>
  6320. <template v-if="data.show">
  6321. <span>{{ data.text }}</span>
  6322. </template>
  6323. </components.VaporChild>
  6324. </template>`,
  6325. {
  6326. VaporChild: {
  6327. code: `<template><slot /></template>`,
  6328. vapor: true,
  6329. },
  6330. },
  6331. data,
  6332. )
  6333. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  6334. data.show = true
  6335. await nextTick()
  6336. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6337. `
  6338. "
  6339. <!--[--><span>foo</span><!--]-->
  6340. "
  6341. `,
  6342. )
  6343. })
  6344. test('hydrate VDOM slot multi-root content can mount after hydrating as empty', async () => {
  6345. const data = reactive({
  6346. show: false,
  6347. text: 'foo',
  6348. })
  6349. const { container } = await testWithVDOMApp(
  6350. `<script setup>
  6351. const data = _data
  6352. const components = _components
  6353. </script>
  6354. <template>
  6355. <components.VaporChild>
  6356. <template v-if="data.show">
  6357. <span>{{ data.text }}</span>
  6358. <span>{{ data.text }}</span>
  6359. </template>
  6360. </components.VaporChild>
  6361. </template>`,
  6362. {
  6363. VaporChild: {
  6364. code: `<template><slot /></template>`,
  6365. vapor: true,
  6366. },
  6367. },
  6368. data,
  6369. )
  6370. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  6371. data.show = true
  6372. await nextTick()
  6373. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6374. `
  6375. "
  6376. <!--[--><span>foo</span><span>foo</span><!--]-->
  6377. "
  6378. `,
  6379. )
  6380. })
  6381. test('hydrate interop vapor slot fallback', async () => {
  6382. const data = reactive({
  6383. text: 'foo',
  6384. })
  6385. const { container } = await testWithVDOMApp(
  6386. `<script setup>
  6387. const components = _components
  6388. </script>
  6389. <template>
  6390. <components.VaporChild />
  6391. </template>`,
  6392. {
  6393. VaporChild: {
  6394. code: `<template><components.VdomChild /></template>`,
  6395. vapor: true,
  6396. },
  6397. VdomChild: {
  6398. code: `<script setup>const data = _data</script>
  6399. <template><slot><span>{{ data.text }}</span></slot></template>`,
  6400. vapor: false,
  6401. },
  6402. },
  6403. data,
  6404. )
  6405. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6406. `
  6407. "
  6408. <!--[--><span>foo</span><!--]-->
  6409. "
  6410. `,
  6411. )
  6412. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  6413. data.text = 'bar'
  6414. await nextTick()
  6415. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6416. `
  6417. "
  6418. <!--[--><span>bar</span><!--]-->
  6419. "
  6420. `,
  6421. )
  6422. })
  6423. test('hydrate interop vapor slot fallback from empty slot branch', async () => {
  6424. const data = reactive({
  6425. show: false,
  6426. fallback: 'foo',
  6427. slot: 'bar',
  6428. })
  6429. const { container } = await testWithVDOMApp(
  6430. `<script setup>
  6431. const components = _components
  6432. </script>
  6433. <template>
  6434. <components.VaporChild />
  6435. </template>`,
  6436. {
  6437. VaporChild: {
  6438. code: `<script setup>
  6439. const data = _data
  6440. const components = _components
  6441. </script>
  6442. <template>
  6443. <components.VdomChild>
  6444. <template #default>
  6445. <template v-if="data.show">
  6446. <span>{{ data.slot }}</span>
  6447. </template>
  6448. </template>
  6449. </components.VdomChild>
  6450. </template>`,
  6451. vapor: true,
  6452. },
  6453. VdomChild: {
  6454. code: `<script setup>const data = _data</script>
  6455. <template><slot><div>{{ data.fallback }}</div></slot></template>`,
  6456. vapor: false,
  6457. },
  6458. },
  6459. data,
  6460. )
  6461. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6462. `
  6463. "
  6464. <!--[--><div>foo</div><!--]-->
  6465. "
  6466. `,
  6467. )
  6468. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  6469. data.fallback = 'baz'
  6470. await nextTick()
  6471. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6472. `
  6473. "
  6474. <!--[--><div>baz</div><!--]-->
  6475. "
  6476. `,
  6477. )
  6478. data.show = true
  6479. await nextTick()
  6480. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6481. `
  6482. "
  6483. <!--[--><span>bar</span><!--]-->
  6484. "
  6485. `,
  6486. )
  6487. data.slot = 'qux'
  6488. await nextTick()
  6489. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6490. `
  6491. "
  6492. <!--[--><span>qux</span><!--]-->
  6493. "
  6494. `,
  6495. )
  6496. })
  6497. test('hydrate interop vapor slot with fallback should preserve valid slot branches', async () => {
  6498. const data = reactive({
  6499. slot: 'bar',
  6500. })
  6501. const { container } = await testWithVDOMApp(
  6502. `<script setup>
  6503. const components = _components
  6504. </script>
  6505. <template>
  6506. <components.VaporChild />
  6507. </template>`,
  6508. {
  6509. VaporChild: {
  6510. code: `<script setup>
  6511. const data = _data
  6512. const components = _components
  6513. </script>
  6514. <template>
  6515. <components.VdomChild>
  6516. <template #default>
  6517. <div>
  6518. <template v-if="false">
  6519. <i>unused</i>
  6520. </template>
  6521. <span>{{ data.slot }}</span>
  6522. </div>
  6523. </template>
  6524. </components.VdomChild>
  6525. </template>`,
  6526. vapor: true,
  6527. },
  6528. VdomChild: {
  6529. code: `<template><slot><p>fallback</p></slot></template>`,
  6530. vapor: false,
  6531. },
  6532. },
  6533. data,
  6534. )
  6535. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6536. `
  6537. "
  6538. <!--[--><div><!----><span>bar</span></div><!--]-->
  6539. "
  6540. `,
  6541. )
  6542. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  6543. data.slot = 'baz'
  6544. await nextTick()
  6545. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6546. `
  6547. "
  6548. <!--[--><div><!----><span>baz</span></div><!--]-->
  6549. "
  6550. `,
  6551. )
  6552. })
  6553. test('hydrate interop vapor multi-root slot fallback from empty slot branch', async () => {
  6554. const data = reactive({
  6555. show: false,
  6556. fallbackA: 'foo',
  6557. fallbackB: 'bar',
  6558. slot: 'baz',
  6559. })
  6560. const { container } = await testWithVDOMApp(
  6561. `<script setup>
  6562. const components = _components
  6563. </script>
  6564. <template>
  6565. <components.VaporChild />
  6566. </template>`,
  6567. {
  6568. VaporChild: {
  6569. code: `<script setup>
  6570. const data = _data
  6571. const components = _components
  6572. </script>
  6573. <template>
  6574. <components.VdomChild>
  6575. <template #default>
  6576. <template v-if="data.show">
  6577. <span>{{ data.slot }}</span>
  6578. </template>
  6579. </template>
  6580. </components.VdomChild>
  6581. </template>`,
  6582. vapor: true,
  6583. },
  6584. VdomChild: {
  6585. code: `<script setup>const data = _data</script>
  6586. <template>
  6587. <slot>
  6588. <div>{{ data.fallbackA }}</div>
  6589. <p>{{ data.fallbackB }}</p>
  6590. </slot>
  6591. </template>`,
  6592. vapor: false,
  6593. },
  6594. },
  6595. data,
  6596. )
  6597. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6598. `
  6599. "
  6600. <!--[--><div>foo</div><p>bar</p><!--]-->
  6601. "
  6602. `,
  6603. )
  6604. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  6605. data.fallbackA = 'qux'
  6606. data.fallbackB = 'quux'
  6607. await nextTick()
  6608. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6609. `
  6610. "
  6611. <!--[--><div>qux</div><p>quux</p><!--]-->
  6612. "
  6613. `,
  6614. )
  6615. data.show = true
  6616. await nextTick()
  6617. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6618. `
  6619. "
  6620. <!--[--><span>baz</span><!--]-->
  6621. "
  6622. `,
  6623. )
  6624. })
  6625. test('hydrate interop vapor multi-root slot fallback should preserve slot anchor on updates', async () => {
  6626. const data = reactive({
  6627. show: false,
  6628. extra: false,
  6629. fallbackA: 'foo',
  6630. fallbackB: 'bar',
  6631. tail: 'tail',
  6632. })
  6633. const { container } = await testWithVDOMApp(
  6634. `<script setup>
  6635. const components = _components
  6636. </script>
  6637. <template>
  6638. <components.VaporChild />
  6639. </template>`,
  6640. {
  6641. VaporChild: {
  6642. code: `<script setup>
  6643. const data = _data
  6644. const components = _components
  6645. </script>
  6646. <template>
  6647. <components.VdomChild>
  6648. <template #default>
  6649. <template v-if="data.show">
  6650. <span>slot</span>
  6651. </template>
  6652. </template>
  6653. </components.VdomChild>
  6654. </template>`,
  6655. vapor: true,
  6656. },
  6657. VdomChild: {
  6658. code: `<script setup>const data = _data</script>
  6659. <template>
  6660. <slot>
  6661. <div>{{ data.fallbackA }}</div>
  6662. <p v-if="data.extra">{{ data.fallbackB }}</p>
  6663. </slot>
  6664. <i>{{ data.tail }}</i>
  6665. </template>`,
  6666. vapor: false,
  6667. },
  6668. },
  6669. data,
  6670. )
  6671. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6672. `
  6673. "
  6674. <!--[-->
  6675. <!--[--><div>foo</div><!----><!--]-->
  6676. <i>tail</i><!--]-->
  6677. "
  6678. `,
  6679. )
  6680. data.extra = true
  6681. await nextTick()
  6682. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6683. `
  6684. "
  6685. <!--[-->
  6686. <!--[--><div>foo</div><p>bar</p><!--]-->
  6687. <i>tail</i><!--]-->
  6688. "
  6689. `,
  6690. )
  6691. })
  6692. test('hydrate interop vapor slot fallback should preserve nested forwarded slots', async () => {
  6693. const data = reactive({
  6694. fallback: 'foo',
  6695. })
  6696. const { container } = await testWithVDOMApp(
  6697. `<script setup>
  6698. const components = _components
  6699. </script>
  6700. <template>
  6701. <components.VaporChild />
  6702. </template>`,
  6703. {
  6704. VaporChild: {
  6705. code: `<template><components.VdomChild /></template>`,
  6706. vapor: true,
  6707. },
  6708. VdomChild: {
  6709. code: `<template><slot><components.Forwarder /></slot></template>`,
  6710. vapor: false,
  6711. },
  6712. Forwarder: {
  6713. code: `<template><components.Receiver><slot /></components.Receiver></template>`,
  6714. vapor: true,
  6715. },
  6716. Receiver: {
  6717. code: `<script setup>const data = _data</script>
  6718. <template><div><slot>{{ data.fallback }}</slot></div></template>`,
  6719. vapor: true,
  6720. },
  6721. },
  6722. data,
  6723. )
  6724. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6725. `
  6726. "
  6727. <!--[--><div>
  6728. <!--[-->foo<!--]-->
  6729. <!--slot--></div><!--]-->
  6730. "
  6731. `,
  6732. )
  6733. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  6734. data.fallback = 'bar'
  6735. await nextTick()
  6736. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6737. `
  6738. "
  6739. <!--[--><div>
  6740. <!--[-->bar<!--]-->
  6741. <!--slot--></div><!--]-->
  6742. "
  6743. `,
  6744. )
  6745. })
  6746. test('hydrate VDOM component returning Fragment', async () => {
  6747. const data = ref('foo')
  6748. const { container } = await testWithVaporApp(
  6749. `<script setup>
  6750. const data = _data; const components = _components;
  6751. </script>
  6752. <template>
  6753. <components.VdomFragmentComp />
  6754. </template>`,
  6755. {
  6756. // VDOM component that returns a Fragment (multiple root nodes)
  6757. VdomFragmentComp: {
  6758. code: `<script setup>const data = _data;</script>
  6759. <template><div>first {{ data }}</div><div>second {{ data }}</div></template>`,
  6760. vapor: false,
  6761. },
  6762. },
  6763. data,
  6764. )
  6765. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6766. `
  6767. "
  6768. <!--[--><div>first foo</div><div>second foo</div><!--]-->
  6769. "
  6770. `,
  6771. )
  6772. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  6773. data.value = 'bar'
  6774. await nextTick()
  6775. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6776. `
  6777. "
  6778. <!--[--><div>first bar</div><div>second bar</div><!--]-->
  6779. "
  6780. `,
  6781. )
  6782. })
  6783. test('hydrate handwritten multi-root VDOM component inside multi-root Vapor component', async () => {
  6784. const first = ref('Hello')
  6785. const second = ref('World')
  6786. // Handwritten VDOM component that returns a Fragment (multi-root)
  6787. const MultiRootVDOM = {
  6788. setup() {
  6789. return () => [
  6790. runtimeDom.h('span', first.value),
  6791. runtimeDom.h('span', second.value),
  6792. ]
  6793. },
  6794. }
  6795. const { container } = await testWithVaporApp(
  6796. `<script setup>
  6797. import { h } from 'vue'
  6798. const MultiRootVDOM = _data.MultiRootVDOM
  6799. </script>
  6800. <template>
  6801. <div>Before</div>
  6802. <MultiRootVDOM />
  6803. <div>After</div>
  6804. </template>`,
  6805. {},
  6806. { MultiRootVDOM },
  6807. )
  6808. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6809. `
  6810. "
  6811. <!--[--><div>Before</div>
  6812. <!--[--><span>Hello</span><span>World</span><!--]-->
  6813. <div>After</div><!--]-->
  6814. "
  6815. `,
  6816. )
  6817. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  6818. first.value = 'Updated'
  6819. second.value = 'Again'
  6820. await nextTick()
  6821. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6822. `
  6823. "
  6824. <!--[--><div>Before</div>
  6825. <!--[--><span>Updated</span><span>Again</span><!--]-->
  6826. <div>After</div><!--]-->
  6827. "
  6828. `,
  6829. )
  6830. })
  6831. test('hydrate handwritten multi-root VDOM component as first child in multi-root Vapor', async () => {
  6832. const MultiRootVDOM = {
  6833. setup() {
  6834. return () => [
  6835. runtimeDom.h('span', 'Hello'),
  6836. runtimeDom.h('span', 'World'),
  6837. ]
  6838. },
  6839. }
  6840. const { container } = await testWithVaporApp(
  6841. `<script setup>
  6842. const MultiRootVDOM = _data.MultiRootVDOM
  6843. </script>
  6844. <template>
  6845. <MultiRootVDOM />
  6846. <div>After</div>
  6847. </template>`,
  6848. {},
  6849. { MultiRootVDOM },
  6850. )
  6851. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6852. `
  6853. "
  6854. <!--[-->
  6855. <!--[--><span>Hello</span><span>World</span><!--]-->
  6856. <div>After</div><!--]-->
  6857. "
  6858. `,
  6859. )
  6860. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  6861. })
  6862. test('hydrate SFC multi-root VDOM component inside multi-root Vapor', async () => {
  6863. const data = ref('foo')
  6864. const { container } = await testWithVaporApp(
  6865. `<script setup>
  6866. const data = _data; const components = _components;
  6867. </script>
  6868. <template>
  6869. <div>Before</div>
  6870. <components.VdomMultiRoot />
  6871. <div>After</div>
  6872. </template>`,
  6873. {
  6874. VdomMultiRoot: {
  6875. code: `<script setup>const data = _data;</script><template><div>first {{ data }}</div><div>second {{ data }}</div></template>`,
  6876. vapor: false,
  6877. },
  6878. },
  6879. data,
  6880. )
  6881. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6882. `
  6883. "
  6884. <!--[--><div>Before</div>
  6885. <!--[--><div>first foo</div><div>second foo</div><!--]-->
  6886. <div>After</div><!--]-->
  6887. "
  6888. `,
  6889. )
  6890. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  6891. data.value = 'bar'
  6892. await nextTick()
  6893. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6894. `
  6895. "
  6896. <!--[--><div>Before</div>
  6897. <!--[--><div>first bar</div><div>second bar</div><!--]-->
  6898. <div>After</div><!--]-->
  6899. "
  6900. `,
  6901. )
  6902. })
  6903. test('hydrate handwritten multi-root VDOM via createDynamicComponent with siblings', async () => {
  6904. const MultiRootVDOM = {
  6905. setup() {
  6906. return () => [
  6907. runtimeDom.h('span', 'Hello'),
  6908. runtimeDom.h('span', 'World'),
  6909. ]
  6910. },
  6911. }
  6912. const { container } = await testWithVaporApp(
  6913. `<script setup>
  6914. import { h } from 'vue'
  6915. const MultiRootVDOM = _data.MultiRootVDOM
  6916. const vnode = h(MultiRootVDOM)
  6917. </script>
  6918. <template>
  6919. <component :is="vnode" />
  6920. <div>After</div>
  6921. </template>`,
  6922. {},
  6923. { MultiRootVDOM },
  6924. )
  6925. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  6926. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6927. `
  6928. "
  6929. <!--[-->
  6930. <!--[--><span>Hello</span><span>World</span><!--]-->
  6931. <!--dynamic-component--><div>After</div><!--]-->
  6932. "
  6933. `,
  6934. )
  6935. })
  6936. test('hydrate handwritten multi-root VDOM via createDynamicComponent as first child of nested multi-root Vapor', async () => {
  6937. const first = ref('Hello')
  6938. const second = ref('World')
  6939. const after = ref('After')
  6940. const MultiRootVDOM = {
  6941. setup() {
  6942. return () => [
  6943. runtimeDom.h('span', first.value),
  6944. runtimeDom.h('span', second.value),
  6945. ]
  6946. },
  6947. }
  6948. const { container } = await testWithVDOMApp(
  6949. `<script setup>
  6950. const components = _components
  6951. </script>
  6952. <template>
  6953. <p>before</p>
  6954. <components.VaporChild />
  6955. </template>`,
  6956. {
  6957. VaporChild: {
  6958. code: `<script setup>
  6959. import { h } from 'vue'
  6960. const MultiRootVDOM = _data.MultiRootVDOM
  6961. const after = _data.after
  6962. const vnode = h(MultiRootVDOM)
  6963. </script>
  6964. <template>
  6965. <component :is="vnode" />
  6966. <div>{{ after }}</div>
  6967. </template>`,
  6968. vapor: true,
  6969. },
  6970. },
  6971. { MultiRootVDOM, after },
  6972. )
  6973. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  6974. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6975. `
  6976. "
  6977. <!--[--><p>before</p>
  6978. <!--[-->
  6979. <!--[--><span>Hello</span><span>World</span><!--]-->
  6980. <!--dynamic-component--><div>After</div><!--]-->
  6981. <!--]-->
  6982. "
  6983. `,
  6984. )
  6985. first.value = 'Updated'
  6986. second.value = 'Again'
  6987. after.value = 'After updated'
  6988. await nextTick()
  6989. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  6990. `
  6991. "
  6992. <!--[--><p>before</p>
  6993. <!--[-->
  6994. <!--[--><span>Updated</span><span>Again</span><!--]-->
  6995. <!--dynamic-component--><div>After updated</div><!--]-->
  6996. <!--]-->
  6997. "
  6998. `,
  6999. )
  7000. })
  7001. test('hydrate multi-root VDOM slot as first child of nested multi-root Vapor', async () => {
  7002. const msg = ref('Hello')
  7003. const after = ref('After')
  7004. const { container } = await testWithVDOMApp(
  7005. `<script setup>
  7006. const components = _components
  7007. const msg = _data.msg
  7008. </script>
  7009. <template>
  7010. <p>before</p>
  7011. <components.VaporChild>
  7012. <components.VdomChild />
  7013. </components.VaporChild>
  7014. </template>`,
  7015. {
  7016. VaporChild: {
  7017. code: `<script setup>
  7018. const after = _data.after
  7019. </script>
  7020. <template>
  7021. <slot />
  7022. <div>{{ after }}</div>
  7023. </template>`,
  7024. vapor: true,
  7025. },
  7026. VdomChild: {
  7027. code: `<script setup>const msg = _data.msg</script>
  7028. <template><span>{{ msg }}</span><span>{{ msg }}</span></template>`,
  7029. vapor: false,
  7030. },
  7031. },
  7032. { msg, after },
  7033. )
  7034. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  7035. `
  7036. "
  7037. <!--[--><p>before</p>
  7038. <!--[-->
  7039. <!--[-->
  7040. <!--[--><span>Hello</span><span>Hello</span><!--]-->
  7041. <!--]-->
  7042. <div>After</div><!--]-->
  7043. <!--]-->
  7044. "
  7045. `,
  7046. )
  7047. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  7048. msg.value = 'Updated'
  7049. after.value = 'After updated'
  7050. await nextTick()
  7051. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  7052. `
  7053. "
  7054. <!--[--><p>before</p>
  7055. <!--[-->
  7056. <!--[-->
  7057. <!--[--><span>Updated</span><span>Updated</span><!--]-->
  7058. <!--]-->
  7059. <div>After updated</div><!--]-->
  7060. <!--]-->
  7061. "
  7062. `,
  7063. )
  7064. })
  7065. test('hydrate slot Fragment as first child of DynamicFragment with trailing sibling', async () => {
  7066. const msg = ref('2')
  7067. const tail = ref('1')
  7068. const { container } = await testWithVDOMApp(
  7069. `<script setup>
  7070. const components = _components
  7071. const msg = _data.msg
  7072. </script>
  7073. <template>
  7074. <components.Comp>{{ msg }}</components.Comp>
  7075. </template>`,
  7076. {
  7077. Comp: {
  7078. code: `<script setup>
  7079. const tail = _data.tail
  7080. </script>
  7081. <template>
  7082. <template v-if="true">
  7083. <slot />
  7084. <span>{{ tail }}</span>
  7085. </template>
  7086. </template>`,
  7087. vapor: true,
  7088. },
  7089. },
  7090. { msg, tail },
  7091. )
  7092. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  7093. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  7094. `
  7095. "
  7096. <!--[-->
  7097. <!--[-->2<!--]-->
  7098. <span>1</span><!--]-->
  7099. "
  7100. `,
  7101. )
  7102. msg.value = '3'
  7103. tail.value = '4'
  7104. await nextTick()
  7105. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  7106. `
  7107. "
  7108. <!--[-->
  7109. <!--[-->3<!--]-->
  7110. <span>4</span><!--]-->
  7111. "
  7112. `,
  7113. )
  7114. })
  7115. test('hydrate VDOM component as first child of DynamicFragment with preceding sibling', async () => {
  7116. const data = ref({
  7117. show: true,
  7118. msg: 'Hello',
  7119. tail: 'Tail',
  7120. })
  7121. const { container } = await testWithVaporApp(
  7122. `<script setup>
  7123. const components = _components
  7124. </script>
  7125. <template>
  7126. <components.VaporChild />
  7127. </template>`,
  7128. {
  7129. VaporChild: {
  7130. code: `<script setup>
  7131. const components = _components
  7132. const data = _data
  7133. </script>
  7134. <template>
  7135. <div>Before</div>
  7136. <template v-if="data.show">
  7137. <components.VdomChild />
  7138. <span>{{ data.tail }}</span>
  7139. </template>
  7140. </template>`,
  7141. vapor: true,
  7142. },
  7143. VdomChild: {
  7144. code: `<script setup>const data = _data</script>
  7145. <template><span>{{ data.msg }}</span><span>{{ data.msg }}</span></template>`,
  7146. vapor: false,
  7147. },
  7148. },
  7149. data,
  7150. )
  7151. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  7152. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  7153. `
  7154. "
  7155. <!--[--><div>Before</div>
  7156. <!--[-->
  7157. <!--[--><span>Hello</span><span>Hello</span><!--]-->
  7158. <span>Tail</span><!--]-->
  7159. <!--]-->
  7160. "
  7161. `,
  7162. )
  7163. data.value.msg = 'Updated'
  7164. data.value.tail = 'Tail updated'
  7165. await nextTick()
  7166. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  7167. `
  7168. "
  7169. <!--[--><div>Before</div>
  7170. <!--[-->
  7171. <!--[--><span>Updated</span><span>Updated</span><!--]-->
  7172. <span>Tail updated</span><!--]-->
  7173. <!--]-->
  7174. "
  7175. `,
  7176. )
  7177. })
  7178. test('hydrate empty slot as first child of DynamicFragment with preceding sibling', async () => {
  7179. const data = ref({
  7180. show: true,
  7181. tail: 'Tail',
  7182. })
  7183. const { container } = await testWithVaporApp(
  7184. `<script setup>
  7185. const components = _components
  7186. </script>
  7187. <template>
  7188. <components.VaporChild />
  7189. </template>`,
  7190. {
  7191. VaporChild: {
  7192. code: `<script setup>
  7193. const data = _data
  7194. </script>
  7195. <template>
  7196. <div>Before</div>
  7197. <template v-if="data.show">
  7198. <slot />
  7199. <span>{{ data.tail }}</span>
  7200. </template>
  7201. </template>`,
  7202. vapor: true,
  7203. },
  7204. },
  7205. data,
  7206. )
  7207. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  7208. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  7209. `
  7210. "
  7211. <!--[--><div>Before</div>
  7212. <!--[-->
  7213. <!--[--><!--]-->
  7214. <span>Tail</span><!--]-->
  7215. <!--]-->
  7216. "
  7217. `,
  7218. )
  7219. data.value.tail = 'Tail updated'
  7220. await nextTick()
  7221. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  7222. `
  7223. "
  7224. <!--[--><div>Before</div>
  7225. <!--[-->
  7226. <!--[--><!--]-->
  7227. <span>Tail updated</span><!--]-->
  7228. <!--]-->
  7229. "
  7230. `,
  7231. )
  7232. })
  7233. test('hydrate empty multi-root if with preceding sibling', async () => {
  7234. const data = ref({
  7235. show: false,
  7236. msg: 'Hello',
  7237. tail: 'Tail',
  7238. })
  7239. const { container } = await testWithVaporApp(
  7240. `<script setup>
  7241. const data = _data
  7242. </script>
  7243. <template>
  7244. <div>Before</div>
  7245. <template v-if="data.show">
  7246. <span>{{ data.msg }}</span>
  7247. <span>{{ data.msg }}</span>
  7248. </template>
  7249. <span>{{ data.tail }}</span>
  7250. </template>`,
  7251. undefined,
  7252. data,
  7253. )
  7254. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  7255. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  7256. `
  7257. "
  7258. <!--[--><div>Before</div><!--if--><span>Tail</span><!--]-->
  7259. "
  7260. `,
  7261. )
  7262. data.value.show = true
  7263. data.value.msg = 'Updated'
  7264. data.value.tail = 'Tail updated'
  7265. await nextTick()
  7266. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  7267. `
  7268. "
  7269. <!--[--><div>Before</div><span>Updated</span><span>Updated</span><!--if--><span>Tail updated</span><!--]-->
  7270. "
  7271. `,
  7272. )
  7273. data.value.show = false
  7274. await nextTick()
  7275. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  7276. `
  7277. "
  7278. <!--[--><div>Before</div><!--if--><span>Tail updated</span><!--]-->
  7279. "
  7280. `,
  7281. )
  7282. })
  7283. test('hydrate multi-root VDOM component as first child of v-for boundary with sibling', async () => {
  7284. const first = ref('Hello')
  7285. const second = ref('World')
  7286. const items = ref([1])
  7287. const MultiRootVDOM = {
  7288. setup() {
  7289. return () => [
  7290. runtimeDom.h('span', first.value),
  7291. runtimeDom.h('span', second.value),
  7292. ]
  7293. },
  7294. }
  7295. const { container } = await testWithVaporApp(
  7296. `<script setup>
  7297. const items = _data.items
  7298. const MultiRootVDOM = _data.MultiRootVDOM
  7299. </script>
  7300. <template>
  7301. <div>Before</div>
  7302. <MultiRootVDOM v-for="item in items" :key="item" />
  7303. <div>After</div>
  7304. </template>`,
  7305. {},
  7306. { items, MultiRootVDOM },
  7307. )
  7308. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  7309. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  7310. `
  7311. "
  7312. <!--[--><div>Before</div>
  7313. <!--[-->
  7314. <!--[--><span>Hello</span><span>World</span><!--]-->
  7315. <!--]-->
  7316. <div>After</div><!--]-->
  7317. "
  7318. `,
  7319. )
  7320. first.value = 'Updated'
  7321. second.value = 'Again'
  7322. await nextTick()
  7323. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  7324. `
  7325. "
  7326. <!--[--><div>Before</div>
  7327. <!--[-->
  7328. <!--[--><span>Updated</span><span>Again</span><!--]-->
  7329. <!--]-->
  7330. <div>After</div><!--]-->
  7331. "
  7332. `,
  7333. )
  7334. })
  7335. test('hydrate multi-root VDOM via mountVNode as non-first child', async () => {
  7336. const MultiRootVDOM = {
  7337. setup() {
  7338. return () => [
  7339. runtimeDom.h('span', 'Hello'),
  7340. runtimeDom.h('span', 'World'),
  7341. ]
  7342. },
  7343. }
  7344. const { container } = await testWithVaporApp(
  7345. `<script setup>
  7346. import { h } from 'vue'
  7347. const MultiRootVDOM = _data.MultiRootVDOM
  7348. const vnode = h(MultiRootVDOM)
  7349. </script>
  7350. <template>
  7351. <div>Before</div>
  7352. <component :is="vnode" />
  7353. <div>After</div>
  7354. </template>`,
  7355. {},
  7356. { MultiRootVDOM },
  7357. )
  7358. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  7359. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  7360. `
  7361. "
  7362. <!--[--><div>Before</div>
  7363. <!--[--><span>Hello</span><span>World</span><!--]-->
  7364. <!--dynamic-component--><div>After</div><!--]-->
  7365. "
  7366. `,
  7367. )
  7368. })
  7369. test('hydrate Fragment VNode as first child of multi-root Vapor via createDynamicComponent', async () => {
  7370. const { container } = await testWithVaporApp(
  7371. `<script setup>
  7372. import { Fragment, h } from 'vue'
  7373. const FragmentChunk = h(Fragment, null, [
  7374. h('div', null, 'first fragment'),
  7375. h('div', null, 'second fragment'),
  7376. ])
  7377. </script>
  7378. <template>
  7379. <component :is="FragmentChunk" />
  7380. <div>After</div>
  7381. </template>`,
  7382. )
  7383. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  7384. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  7385. `
  7386. "
  7387. <!--[-->
  7388. <!--[--><div>first fragment</div><div>second fragment</div><!--]-->
  7389. <!--dynamic-component--><div>After</div><!--]-->
  7390. "
  7391. `,
  7392. )
  7393. })
  7394. test('hydrate Element VNode as first child of multi-root Vapor via createDynamicComponent', async () => {
  7395. const { container } = await testWithVaporApp(
  7396. `<script setup>
  7397. import { h } from 'vue'
  7398. const elementVNode = h('span', null, 'hello')
  7399. </script>
  7400. <template>
  7401. <component :is="elementVNode" />
  7402. <div>After</div>
  7403. </template>`,
  7404. )
  7405. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  7406. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  7407. `
  7408. "
  7409. <!--[--><span>hello</span><!--dynamic-component--><div>After</div><!--]-->
  7410. "
  7411. `,
  7412. )
  7413. })
  7414. test('hydrate interop vapor slot fallback from empty slot branch under Suspense', async () => {
  7415. const data = reactive({
  7416. show: false,
  7417. fallback: 'foo',
  7418. slot: 'bar',
  7419. })
  7420. const { container } = await testWithVDOMApp(
  7421. `<script setup>
  7422. const components = _components
  7423. </script>
  7424. <template>
  7425. <Suspense>
  7426. <components.VaporChild />
  7427. <template #fallback>
  7428. <i>pending</i>
  7429. </template>
  7430. </Suspense>
  7431. </template>`,
  7432. {
  7433. VaporChild: {
  7434. code: `<script setup>
  7435. const data = _data
  7436. const components = _components
  7437. </script>
  7438. <template>
  7439. <components.VdomChild>
  7440. <template #default>
  7441. <template v-if="data.show">
  7442. <span>{{ data.slot }}</span>
  7443. </template>
  7444. </template>
  7445. </components.VdomChild>
  7446. </template>`,
  7447. vapor: true,
  7448. },
  7449. VdomChild: {
  7450. code: `<script setup>const data = _data</script>
  7451. <template><slot><div>{{ data.fallback }}</div></slot></template>`,
  7452. vapor: false,
  7453. },
  7454. },
  7455. data,
  7456. )
  7457. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  7458. `
  7459. "
  7460. <!--[--><div>foo</div><!--]-->
  7461. "
  7462. `,
  7463. )
  7464. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  7465. data.fallback = 'baz'
  7466. await nextTick()
  7467. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  7468. `
  7469. "
  7470. <!--[--><div>baz</div><!--]-->
  7471. "
  7472. `,
  7473. )
  7474. data.show = true
  7475. await nextTick()
  7476. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  7477. `
  7478. "
  7479. <!--[--><span>bar</span><!--]-->
  7480. "
  7481. `,
  7482. )
  7483. data.slot = 'qux'
  7484. await nextTick()
  7485. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  7486. `
  7487. "
  7488. <!--[--><span>qux</span><!--]-->
  7489. "
  7490. `,
  7491. )
  7492. })
  7493. })