hydration.spec.ts 178 KB

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