hydration.spec.ts 140 KB

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