hydration.spec.ts 158 KB

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