componentSlots.spec.ts 127 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298
  1. // NOTE: This test is implemented based on the case of `runtime-core/__test__/componentSlots.spec.ts`.
  2. import {
  3. VaporTeleport,
  4. child,
  5. createComponent,
  6. createFor,
  7. createForSlots,
  8. createIf,
  9. createSlot,
  10. createVaporApp,
  11. defineVaporComponent,
  12. insert,
  13. prepend,
  14. remove,
  15. renderEffect,
  16. setInsertionState,
  17. template,
  18. txt,
  19. vaporInteropPlugin,
  20. withVaporCtx,
  21. } from '../src'
  22. import {
  23. type Ref,
  24. createApp,
  25. createSlots,
  26. currentInstance,
  27. h,
  28. nextTick,
  29. onScopeDispose,
  30. ref,
  31. renderSlot,
  32. shallowRef,
  33. toDisplayString,
  34. } from '@vue/runtime-dom'
  35. import { makeRender } from './_utils'
  36. import type { DynamicSlot } from '../src/componentSlots'
  37. import { setElementText, setText } from '../src/dom/prop'
  38. import { type Block, isValidBlock } from '../src/block'
  39. import { hydrateNode, setCurrentHydrationNode } from '../src/dom/hydration'
  40. import {
  41. DynamicFragment,
  42. ForFragment,
  43. type SlotBoundaryContext,
  44. SlotFallbackController,
  45. SlotFragment,
  46. VaporFragment,
  47. getCurrentSlotBoundary,
  48. getCurrentSlotEndAnchor,
  49. isHydratingSlotFallbackActive,
  50. trackSlotBoundaryDirtying,
  51. withHydratingSlotBoundary,
  52. withHydratingSlotFallbackActive,
  53. withOwnedSlotBoundary,
  54. withSlotFallbackBoundary,
  55. } from '../src/fragment'
  56. const define = makeRender<any>()
  57. function renderWithSlots(slots: any): any {
  58. let instance: any
  59. const Comp = defineVaporComponent({
  60. setup() {
  61. const t0 = template('<div></div>')
  62. const n0 = t0()
  63. instance = currentInstance
  64. return n0
  65. },
  66. })
  67. const { render } = define({
  68. render() {
  69. return createComponent(Comp, {}, slots)
  70. },
  71. })
  72. render()
  73. return instance
  74. }
  75. describe('component: slots', () => {
  76. test('initSlots: instance.slots should be set correctly', () => {
  77. const { slots } = renderWithSlots({
  78. default: () => template('<span></span>')(),
  79. })
  80. expect(slots.default()).toMatchObject(document.createElement('span'))
  81. })
  82. test('updateSlots: instance.slots should be updated correctly', async () => {
  83. const flag1 = ref(true)
  84. let instance: any
  85. const Child = () => {
  86. instance = currentInstance
  87. return template('child')()
  88. }
  89. const { render } = define({
  90. render() {
  91. return createComponent(
  92. Child,
  93. {},
  94. {
  95. $: [
  96. () =>
  97. flag1.value
  98. ? { name: 'one', fn: () => template('<span></span>')() }
  99. : { name: 'two', fn: () => template('<div></div>')() },
  100. ],
  101. },
  102. )
  103. },
  104. })
  105. render()
  106. expect(instance.slots).toHaveProperty('one')
  107. expect(instance.slots).not.toHaveProperty('two')
  108. flag1.value = false
  109. await nextTick()
  110. expect(instance.slots).not.toHaveProperty('one')
  111. expect(instance.slots).toHaveProperty('two')
  112. })
  113. // passes but no warning for slot invocation in vapor currently
  114. test.todo('should not warn when mounting another app in setup', () => {
  115. const Comp = defineVaporComponent({
  116. setup(_, { slots }) {
  117. return slots.default!()
  118. },
  119. })
  120. const mountComp = () => {
  121. createVaporApp({
  122. render() {
  123. return createComponent(
  124. Comp,
  125. {},
  126. { default: () => template('msg')() },
  127. )!
  128. },
  129. })
  130. }
  131. const App = {
  132. setup() {
  133. mountComp()
  134. return []
  135. },
  136. }
  137. createVaporApp(App).mount(document.createElement('div'))
  138. expect(
  139. 'Slot "default" invoked outside of the render function',
  140. ).not.toHaveBeenWarned()
  141. })
  142. describe('slot fallback boundary', () => {
  143. test('slot fragment insert uses active fallback output', () => {
  144. const container = document.createElement('div')
  145. const frag = new SlotFragment()
  146. frag.updateSlot(undefined, () => document.createTextNode('fallback'))
  147. insert(frag, container)
  148. expect(container.innerHTML).toBe('fallback<!--slot-->')
  149. })
  150. test('slot fragment validity uses active fallback output', () => {
  151. const frag = new SlotFragment()
  152. frag.updateSlot(undefined, () => document.createTextNode('fallback'))
  153. expect(isValidBlock(frag)).toBe(true)
  154. })
  155. test('slot fragment validityPending takes precedence over effective output', () => {
  156. const frag = new SlotFragment()
  157. frag.validityPending = true
  158. expect(isValidBlock(frag)).toBe(true)
  159. })
  160. test('slot fragment remove cleans active fallback and fallback scope', () => {
  161. const container = document.createElement('div')
  162. const stop = vi.fn()
  163. const frag = new SlotFragment()
  164. frag.updateSlot(undefined, () => {
  165. onScopeDispose(stop)
  166. return document.createTextNode('fallback')
  167. })
  168. insert(frag, container)
  169. remove(frag, container)
  170. expect(Array.from(container.childNodes)).toEqual([])
  171. expect(stop).toHaveBeenCalledTimes(1)
  172. })
  173. test('slot fragment prefers local fallback over inherited fallback', () => {
  174. const localFallback = vi.fn(() =>
  175. document.createTextNode('local fallback'),
  176. )
  177. const inheritedFallback = vi.fn(() =>
  178. document.createTextNode('inherited fallback'),
  179. )
  180. const frag = new SlotFragment()
  181. frag.parentSlotBoundary = {
  182. parent: null,
  183. getLocalFallback: () => inheritedFallback,
  184. markDirty: vi.fn(),
  185. }
  186. frag.updateSlot(undefined, localFallback)
  187. expect(frag.fallbackBlock).toBeInstanceOf(Text)
  188. expect((frag.fallbackBlock as Text).textContent).toBe('local fallback')
  189. expect(localFallback).toHaveBeenCalled()
  190. expect(inheritedFallback).not.toHaveBeenCalled()
  191. })
  192. test('slot fragment local fallback renders nested slots against the parent boundary', () => {
  193. const parentBoundary = {
  194. parent: null,
  195. getLocalFallback: () => () => document.createTextNode('outer fallback'),
  196. markDirty: vi.fn(),
  197. }
  198. const frag = new SlotFragment()
  199. frag.parentSlotBoundary = parentBoundary
  200. let fallbackBoundary: any
  201. frag.updateSlot(undefined, () => {
  202. fallbackBoundary = getCurrentSlotBoundary()
  203. return []
  204. })
  205. // Fallback body renders under a redirected boundary whose `.parent` is
  206. // the owning boundary's parent — so nested slots inherit from the
  207. // grandparent, avoiding fallback -> <slot> -> same fallback recursion.
  208. expect(fallbackBoundary).not.toBe(frag.slotFallbackBoundary)
  209. expect(fallbackBoundary.parent).toBe(parentBoundary)
  210. })
  211. test('withSlotFallbackBoundary reuses the same redirected boundary', () => {
  212. const parentBoundaryA = {
  213. parent: null,
  214. getLocalFallback: () => undefined,
  215. markDirty: vi.fn(),
  216. }
  217. const parentBoundaryB = {
  218. parent: null,
  219. getLocalFallback: () => undefined,
  220. markDirty: vi.fn(),
  221. }
  222. let activeParent = parentBoundaryA
  223. const boundary: SlotBoundaryContext = {
  224. get parent() {
  225. return activeParent
  226. },
  227. getLocalFallback: () => () => document.createTextNode('fallback'),
  228. markDirty: vi.fn(),
  229. }
  230. let firstBoundary!: SlotBoundaryContext | null
  231. let secondBoundary!: SlotBoundaryContext | null
  232. withSlotFallbackBoundary(boundary, () => {
  233. firstBoundary = getCurrentSlotBoundary()
  234. })
  235. activeParent = parentBoundaryB
  236. withSlotFallbackBoundary(boundary, () => {
  237. secondBoundary = getCurrentSlotBoundary()
  238. })
  239. expect(firstBoundary).toBe(secondBoundary)
  240. expect(firstBoundary!.parent).toBe(parentBoundaryB)
  241. expect(firstBoundary!.getLocalFallback()).toBeUndefined()
  242. })
  243. test('slot fragment local fallback keeps itself as owner for nested fragments', () => {
  244. const container = document.createElement('div')
  245. const parentBoundary = {
  246. parent: null,
  247. getLocalFallback: () => () => document.createTextNode('outer fallback'),
  248. markDirty: vi.fn(),
  249. }
  250. const frag = new SlotFragment()
  251. const child = new DynamicFragment('if', false, false)
  252. let initialized = false
  253. frag.parentSlotBoundary = parentBoundary
  254. frag.updateSlot(undefined, () => {
  255. if (!initialized) {
  256. initialized = true
  257. trackSlotBoundaryDirtying(child)
  258. child.update(() => document.createTextNode('inner fallback'))
  259. }
  260. return child
  261. })
  262. insert(frag, container)
  263. expect(container.innerHTML).toBe('inner fallback<!--if--><!--slot-->')
  264. child.update(() => [])
  265. expect(container.innerHTML).toBe('outer fallback<!--if--><!--slot-->')
  266. })
  267. test('slot fragment local fallback ignores unrelated ancestor fallback refs', async () => {
  268. const ancestorText = ref('outer fallback')
  269. const localFallback = vi.fn(() =>
  270. document.createTextNode('local fallback'),
  271. )
  272. const container = document.createElement('div')
  273. const frag = new SlotFragment()
  274. const parentBoundary = {
  275. parent: null,
  276. getLocalFallback: () => () =>
  277. document.createTextNode(ancestorText.value),
  278. markDirty: vi.fn(),
  279. }
  280. frag.parentSlotBoundary = parentBoundary
  281. frag.updateSlot(undefined, localFallback)
  282. insert(frag, container)
  283. expect(container.innerHTML).toBe('local fallback<!--slot-->')
  284. expect(localFallback).toHaveBeenCalledTimes(1)
  285. ancestorText.value = 'updated outer fallback'
  286. await nextTick()
  287. expect(container.innerHTML).toBe('local fallback<!--slot-->')
  288. expect(localFallback).toHaveBeenCalledTimes(1)
  289. })
  290. test('slot fragment activates local fallback while preserving content carrier', () => {
  291. const container = document.createElement('div')
  292. const content = new DynamicFragment('if', false, false)
  293. const frag = new SlotFragment()
  294. frag.updateSlot(
  295. () => content,
  296. () => document.createTextNode('fallback'),
  297. )
  298. insert(frag, container)
  299. expect(container.innerHTML).toBe('fallback<!--if--><!--slot-->')
  300. })
  301. test('slot fragment activates local fallback while preserving v-for carrier', () => {
  302. const container = document.createElement('div')
  303. const content = new ForFragment([[], document.createComment('for')])
  304. const frag = new SlotFragment()
  305. frag.updateSlot(
  306. () => content,
  307. () => document.createTextNode('fallback'),
  308. )
  309. insert(frag, container)
  310. expect(container.innerHTML).toBe('fallback<!--for--><!--slot-->')
  311. })
  312. test('slot fragment delays fallback activation until pending child validity resolves', () => {
  313. const container = document.createElement('div')
  314. const frag = new SlotFragment()
  315. let child!: VaporFragment
  316. frag.updateSlot(
  317. () => {
  318. child = new VaporFragment([])
  319. child.validityPending = true
  320. trackSlotBoundaryDirtying(child)
  321. return child
  322. },
  323. () => document.createTextNode('fallback'),
  324. )
  325. insert(frag, container)
  326. expect(container.innerHTML).toBe('<!--slot-->')
  327. child.validityPending = false
  328. child.nodes = []
  329. child.onUpdated!.forEach(hook => hook())
  330. expect(container.innerHTML).toBe('fallback<!--slot-->')
  331. })
  332. test('slot fallback controller ignores dirty notifications after dispose', () => {
  333. let disposed = false
  334. const controller = new SlotFallbackController({
  335. getParentBoundary: () => null,
  336. getLocalFallback: () => undefined,
  337. getContent: () => [],
  338. getParentNode: () => null,
  339. getAnchor: () => null,
  340. runWithRenderCtx: fn => fn(),
  341. isDisposed: () => disposed,
  342. onValidityChange: vi.fn(),
  343. })
  344. disposed = true
  345. controller.boundary.markDirty()
  346. expect(controller.takePendingRecheck()).toBe(false)
  347. })
  348. test('withHydratingSlotBoundary isolates fallback-active state between boundaries without local markers', () => {
  349. const start = document.createComment('[')
  350. const end = document.createComment(']')
  351. const host = document.createElement('div')
  352. host.append(start, end)
  353. hydrateNode(start, () => {
  354. withHydratingSlotBoundary(() => {
  355. const outerEnd = getCurrentSlotEndAnchor()
  356. expect(outerEnd).toBe(end)
  357. setCurrentHydrationNode(end)
  358. withHydratingSlotBoundary(() => {
  359. expect(getCurrentSlotEndAnchor()).toBe(end)
  360. expect(isHydratingSlotFallbackActive()).toBe(false)
  361. withHydratingSlotFallbackActive(() => {
  362. expect(isHydratingSlotFallbackActive()).toBe(true)
  363. setCurrentHydrationNode(end)
  364. withHydratingSlotBoundary(() => {
  365. expect(getCurrentSlotEndAnchor()).toBe(end)
  366. expect(isHydratingSlotFallbackActive()).toBe(false)
  367. })
  368. })
  369. expect(getCurrentSlotEndAnchor()).toBe(end)
  370. expect(isHydratingSlotFallbackActive()).toBe(false)
  371. })
  372. expect(getCurrentSlotEndAnchor()).toBe(end)
  373. expect(isHydratingSlotFallbackActive()).toBe(false)
  374. })
  375. })
  376. })
  377. test('slot fallback controller stops fallback scope when fallback body throws', async () => {
  378. const source = ref(0)
  379. const effectRuns = vi.fn()
  380. const cleanup = vi.fn()
  381. const err = new Error('fallback boom')
  382. const controller = new SlotFallbackController({
  383. getParentBoundary: () => null,
  384. getLocalFallback: () => () => {
  385. onScopeDispose(cleanup)
  386. renderEffect(() => {
  387. effectRuns(source.value)
  388. })
  389. throw err
  390. },
  391. getContent: () => [],
  392. getParentNode: () => null,
  393. getAnchor: () => null,
  394. runWithRenderCtx: fn => fn(),
  395. onValidityChange: vi.fn(),
  396. })
  397. expect(() => controller.recheck()).toThrow(err)
  398. expect(controller.getActiveFallback()).toBe(null)
  399. expect(cleanup).toHaveBeenCalledTimes(1)
  400. expect(effectRuns).toHaveBeenCalledTimes(1)
  401. source.value++
  402. await nextTick()
  403. expect(effectRuns).toHaveBeenCalledTimes(1)
  404. })
  405. test('slot fallback controller does not accumulate order-sync hooks', async () => {
  406. const fallback = new VaporFragment([
  407. document.createTextNode('a'),
  408. document.createTextNode('b'),
  409. ])
  410. const controller = new SlotFallbackController({
  411. getParentBoundary: () => null,
  412. getLocalFallback: () => () => fallback,
  413. getContent: () => [],
  414. getParentNode: () => null,
  415. getAnchor: () => null,
  416. runWithRenderCtx: fn => fn(),
  417. onValidityChange: vi.fn(),
  418. })
  419. controller.recheck()
  420. expect(fallback.onUpdated).toHaveLength(1)
  421. controller.syncActiveFallback()
  422. await nextTick()
  423. expect(fallback.onUpdated).toHaveLength(1)
  424. })
  425. test('slot fallback controller re-syncs the whole carrier block order', async () => {
  426. const container = document.createElement('div')
  427. const carrierA = document.createTextNode('x')
  428. const carrierB = document.createTextNode('y')
  429. const marker = document.createTextNode('!')
  430. const slotAnchor = document.createComment('slot')
  431. const fallback = new VaporFragment([
  432. document.createTextNode('a'),
  433. document.createTextNode('b'),
  434. ])
  435. const controller = new SlotFallbackController({
  436. getParentBoundary: () => null,
  437. getLocalFallback: () => () => fallback,
  438. getContent: () => [carrierA, carrierB],
  439. getParentNode: () => container,
  440. getAnchor: () => slotAnchor,
  441. runWithRenderCtx: fn => fn(),
  442. isContentValid: () => false,
  443. onValidityChange: vi.fn(),
  444. })
  445. container.append(carrierA, marker, carrierB, slotAnchor)
  446. controller.recheck()
  447. expect(container.innerHTML).toBe('abx!y<!--slot-->')
  448. controller.syncActiveFallback()
  449. await nextTick()
  450. expect(container.innerHTML).toBe('abxy!<!--slot-->')
  451. })
  452. test('slot fallback controller re-syncs carrier order when fallback ends with a fragment anchor', async () => {
  453. const container = document.createElement('div')
  454. const carrierA = document.createTextNode('x')
  455. const carrierB = document.createTextNode('y')
  456. const marker = document.createTextNode('!')
  457. const slotAnchor = document.createComment('slot')
  458. const trailingFragment = new DynamicFragment('if', false, false)
  459. trailingFragment.update(() => document.createTextNode('b'))
  460. const fallback = new VaporFragment<Block>([
  461. document.createTextNode('a'),
  462. trailingFragment,
  463. ])
  464. const controller = new SlotFallbackController({
  465. getParentBoundary: () => null,
  466. getLocalFallback: () => () => fallback,
  467. getContent: () => [carrierA, carrierB],
  468. getParentNode: () => container,
  469. getAnchor: () => slotAnchor,
  470. runWithRenderCtx: fn => fn(),
  471. isContentValid: () => false,
  472. onValidityChange: vi.fn(),
  473. })
  474. container.append(carrierA, marker, carrierB, slotAnchor)
  475. controller.recheck()
  476. expect(container.innerHTML).toBe('ab<!--if-->x!y<!--slot-->')
  477. controller.syncActiveFallback()
  478. await nextTick()
  479. expect(container.innerHTML).toBe('ab<!--if-->xy!<!--slot-->')
  480. })
  481. test('slot fallback controller defaults to idle when isBusy is omitted', () => {
  482. const fallback = document.createTextNode('fallback')
  483. const controller = new SlotFallbackController({
  484. getParentBoundary: () => null,
  485. getLocalFallback: () => () => fallback,
  486. getContent: () => [],
  487. getParentNode: () => null,
  488. getAnchor: () => null,
  489. runWithRenderCtx: fn => fn(),
  490. onValidityChange: vi.fn(),
  491. })
  492. controller.boundary.markDirty()
  493. expect(controller.getActiveFallback()).toBe(fallback)
  494. })
  495. test('vdom slot dirties parent boundary once when content stays valid', async () => {
  496. const text = ref('A')
  497. const boundary = {
  498. parent: null,
  499. getLocalFallback: () => undefined,
  500. markDirty: vi.fn(),
  501. }
  502. const instance = renderWithSlots({})
  503. const app = createApp({ render: () => null })
  504. app.use(vaporInteropPlugin)
  505. const vapor = (app._context as any).vapor
  506. const slotsRef = shallowRef({
  507. default: () => [h('div', text.value)],
  508. })
  509. const frag = withOwnedSlotBoundary(boundary, () =>
  510. vapor.vdomSlot(slotsRef, 'default', {}, instance),
  511. )
  512. const host = document.createElement('div')
  513. insert(frag, host)
  514. boundary.markDirty.mockClear()
  515. text.value = 'B'
  516. await nextTick()
  517. expect(host.innerHTML).toContain('<div>B</div>')
  518. expect(boundary.markDirty).toHaveBeenCalledTimes(1)
  519. })
  520. test('vdom slot dirties parent boundary once when switching from valid content to local fallback', async () => {
  521. const show = ref(true)
  522. const boundary = {
  523. parent: null,
  524. getLocalFallback: () => undefined,
  525. markDirty: vi.fn(),
  526. }
  527. const instance = renderWithSlots({})
  528. const app = createApp({ render: () => null })
  529. app.use(vaporInteropPlugin)
  530. const vapor = (app._context as any).vapor
  531. const slotsRef = shallowRef({
  532. default: () => (show.value ? [h('div', 'content')] : []),
  533. })
  534. const frag = withOwnedSlotBoundary(boundary, () =>
  535. vapor.vdomSlot(slotsRef, 'default', {}, instance, () =>
  536. template('fallback')(),
  537. ),
  538. )
  539. const host = document.createElement('div')
  540. insert(frag, host)
  541. boundary.markDirty.mockClear()
  542. show.value = false
  543. await nextTick()
  544. expect(host.innerHTML).toBe('fallback')
  545. expect(boundary.markDirty).toHaveBeenCalledTimes(1)
  546. })
  547. test('vdom slot still renders vapor fallback when slot content resolves empty', () => {
  548. const Child = defineVaporComponent({
  549. setup() {
  550. return createSlot('default', null, () => template('child fallback')())
  551. },
  552. })
  553. const root = document.createElement('div')
  554. createApp({
  555. render: () =>
  556. h(Child as any, null, {
  557. default: () => [],
  558. }),
  559. })
  560. .use(vaporInteropPlugin)
  561. .mount(root)
  562. expect(root.innerHTML).toBe('child fallback')
  563. })
  564. })
  565. describe('createSlot', () => {
  566. test('slot should be rendered correctly', () => {
  567. const Comp = defineVaporComponent(() => {
  568. const n0 = template('<div>')()
  569. insert(createSlot('header'), n0 as any as ParentNode)
  570. return n0
  571. })
  572. const { host } = define(() => {
  573. return createComponent(Comp, null, {
  574. header: () => template('header')(),
  575. })
  576. }).render()
  577. expect(host.innerHTML).toBe('<div>header<!--slot--></div>')
  578. })
  579. test('slot should be rendered correctly with slot props', async () => {
  580. const src = ref('header')
  581. const Comp = defineVaporComponent(() => {
  582. const n0 = template('<div></div>')()
  583. insert(
  584. createSlot('header', { title: () => src.value }),
  585. n0 as any as ParentNode,
  586. )
  587. return n0
  588. })
  589. const { host } = define(() => {
  590. return createComponent(Comp, null, {
  591. header: props => {
  592. const el = template('<h1></h1>')()
  593. renderEffect(() => {
  594. setElementText(el, props.title)
  595. })
  596. return el
  597. },
  598. })
  599. }).render()
  600. expect(host.innerHTML).toBe('<div><h1>header</h1><!--slot--></div>')
  601. src.value = 'footer'
  602. await nextTick()
  603. expect(host.innerHTML).toBe('<div><h1>footer</h1><!--slot--></div>')
  604. })
  605. test('slot props should be isolated per fragment in v-for', async () => {
  606. const items = ref([0, 1, 2])
  607. const Child = defineVaporComponent(() => {
  608. const list = createFor(
  609. () => items.value,
  610. for_item0 => {
  611. const n0 = template('<div></div>')()
  612. insert(
  613. createSlot('age-option', { age: () => for_item0.value }),
  614. n0 as any as ParentNode,
  615. )
  616. return n0
  617. },
  618. )
  619. return list
  620. })
  621. const { host } = define(() => {
  622. return createComponent(Child, null, {
  623. 'age-option': (props: any) => {
  624. const el = template('<span></span>')()
  625. renderEffect(() => {
  626. setElementText(el, toDisplayString(props.age))
  627. })
  628. return el
  629. },
  630. })
  631. }).render()
  632. expect(host.innerHTML).toBe(
  633. '<div><span>0</span><!--slot--></div>' +
  634. '<div><span>1</span><!--slot--></div>' +
  635. '<div><span>2</span><!--slot--></div><!--for-->',
  636. )
  637. items.value = [3, 4]
  638. await nextTick()
  639. expect(host.innerHTML).toBe(
  640. '<div><span>3</span><!--slot--></div>' +
  641. '<div><span>4</span><!--slot--></div><!--for-->',
  642. )
  643. })
  644. test('dynamic slot props', async () => {
  645. let props: any
  646. const bindObj = ref<Record<string, any>>({ foo: 1, baz: 'qux' })
  647. const Comp = defineVaporComponent(() =>
  648. createSlot('default', { $: [() => bindObj.value] }),
  649. )
  650. define(() =>
  651. createComponent(Comp, null, {
  652. default: (_props: any) => ((props = _props), []),
  653. }),
  654. ).render()
  655. expect(props).toEqual({ foo: 1, baz: 'qux' })
  656. bindObj.value.foo = 2
  657. await nextTick()
  658. expect(props).toEqual({ foo: 2, baz: 'qux' })
  659. delete bindObj.value.baz
  660. await nextTick()
  661. expect(props).toEqual({ foo: 2 })
  662. })
  663. test('dynamic slot props with static slot props', async () => {
  664. let props: any
  665. const foo = ref(0)
  666. const bindObj = ref<Record<string, any>>({ foo: 100, baz: 'qux' })
  667. const Comp = defineVaporComponent(() =>
  668. createSlot('default', {
  669. foo: () => foo.value,
  670. $: [() => bindObj.value],
  671. }),
  672. )
  673. define(() =>
  674. createComponent(Comp, null, {
  675. default: (_props: any) => ((props = _props), []),
  676. }),
  677. ).render()
  678. expect(props).toEqual({ foo: 100, baz: 'qux' })
  679. foo.value = 2
  680. await nextTick()
  681. expect(props).toEqual({ foo: 100, baz: 'qux' })
  682. delete bindObj.value.foo
  683. await nextTick()
  684. expect(props).toEqual({ foo: 2, baz: 'qux' })
  685. })
  686. test('dynamic slot should be rendered correctly with slot props', async () => {
  687. const val = ref('header')
  688. const Comp = defineVaporComponent(() => {
  689. const n0 = template('<div></div>')()
  690. prepend(
  691. n0 as any as ParentNode,
  692. createSlot('header', { title: () => val.value }),
  693. )
  694. return n0
  695. })
  696. const { host } = define(() => {
  697. // dynamic slot
  698. return createComponent(Comp, null, {
  699. $: [
  700. () => ({
  701. name: 'header',
  702. fn: (props: any) => {
  703. const el = template('<h1></h1>')()
  704. renderEffect(() => {
  705. setElementText(el, props.title)
  706. })
  707. return el
  708. },
  709. }),
  710. ],
  711. })
  712. }).render()
  713. expect(host.innerHTML).toBe('<div><h1>header</h1><!--slot--></div>')
  714. val.value = 'footer'
  715. await nextTick()
  716. expect(host.innerHTML).toBe('<div><h1>footer</h1><!--slot--></div>')
  717. })
  718. test('dynamic slot outlet should be render correctly with slot props', async () => {
  719. const val = ref('header')
  720. const Comp = defineVaporComponent(() => {
  721. const n0 = template('<div></div>')()
  722. prepend(
  723. n0 as any as ParentNode,
  724. createSlot(
  725. () => val.value, // dynamic slot outlet name
  726. ),
  727. )
  728. return n0
  729. })
  730. const { host } = define(() => {
  731. return createComponent(Comp, null, {
  732. header: () => template('header')(),
  733. footer: () => template('footer')(),
  734. })
  735. }).render()
  736. expect(host.innerHTML).toBe('<div>header<!--slot--></div>')
  737. val.value = 'footer'
  738. await nextTick()
  739. expect(host.innerHTML).toBe('<div>footer<!--slot--></div>')
  740. })
  741. test('fallback should be render correctly', () => {
  742. const Comp = defineVaporComponent(() => {
  743. const n0 = template('<div></div>')()
  744. insert(
  745. createSlot('header', undefined, () => template('fallback')()),
  746. n0 as any as ParentNode,
  747. )
  748. return n0
  749. })
  750. const { host } = define(() => {
  751. return createComponent(Comp, {}, {})
  752. }).render()
  753. expect(host.innerHTML).toBe('<div>fallback<!--slot--></div>')
  754. })
  755. test('dynamic slot should be updated correctly', async () => {
  756. const flag1 = ref(true)
  757. const Child = defineVaporComponent(() => {
  758. const temp0 = template('<p></p>')
  759. const el0 = temp0()
  760. const el1 = temp0()
  761. const slot1 = createSlot('one', null, () => template('one fallback')())
  762. const slot2 = createSlot('two', null, () => template('two fallback')())
  763. insert(slot1, el0 as any as ParentNode)
  764. insert(slot2, el1 as any as ParentNode)
  765. return [el0, el1]
  766. })
  767. const { host } = define(() => {
  768. return createComponent(Child, null, {
  769. $: [
  770. () =>
  771. flag1.value
  772. ? {
  773. name: 'one',
  774. fn: () => template('one content')(),
  775. }
  776. : {
  777. name: 'two',
  778. fn: () => template('two content')(),
  779. },
  780. ],
  781. })
  782. }).render()
  783. expect(host.innerHTML).toBe(
  784. '<p>one content<!--slot--></p><p>two fallback<!--slot--></p>',
  785. )
  786. flag1.value = false
  787. await nextTick()
  788. expect(host.innerHTML).toBe(
  789. '<p>one fallback<!--slot--></p><p>two content<!--slot--></p>',
  790. )
  791. flag1.value = true
  792. await nextTick()
  793. expect(host.innerHTML).toBe(
  794. '<p>one content<!--slot--></p><p>two fallback<!--slot--></p>',
  795. )
  796. })
  797. test('dynamic slot outlet should be updated correctly', async () => {
  798. const slotOutletName = ref('one')
  799. const Child = defineVaporComponent(() => {
  800. const temp0 = template('<p>')
  801. const el0 = temp0()
  802. const slot1 = createSlot(
  803. () => slotOutletName.value,
  804. undefined,
  805. () => template('fallback')(),
  806. )
  807. insert(slot1, el0 as any as ParentNode)
  808. return el0
  809. })
  810. const { host } = define(() => {
  811. return createComponent(
  812. Child,
  813. {},
  814. {
  815. one: () => template('one content')(),
  816. two: () => template('two content')(),
  817. },
  818. )
  819. }).render()
  820. expect(host.innerHTML).toBe('<p>one content<!--slot--></p>')
  821. slotOutletName.value = 'two'
  822. await nextTick()
  823. expect(host.innerHTML).toBe('<p>two content<!--slot--></p>')
  824. slotOutletName.value = 'none'
  825. await nextTick()
  826. expect(host.innerHTML).toBe('<p>fallback<!--slot--></p>')
  827. })
  828. test('non-exist slot', async () => {
  829. const Child = defineVaporComponent(() => {
  830. const el0 = template('<p>')()
  831. const slot = createSlot('not-exist', undefined)
  832. insert(slot, el0 as any as ParentNode)
  833. return el0
  834. })
  835. const { host } = define(() => {
  836. return createComponent(Child)
  837. }).render()
  838. expect(host.innerHTML).toBe('<p><!--slot--></p>')
  839. })
  840. test('use fallback when inner content changes', async () => {
  841. const Child = {
  842. setup() {
  843. return createSlot('default', null, () =>
  844. document.createTextNode('fallback'),
  845. )
  846. },
  847. }
  848. const toggle = ref(true)
  849. const { html } = define({
  850. setup() {
  851. return createComponent(Child, null, {
  852. default: () => {
  853. return createIf(
  854. () => toggle.value,
  855. () => {
  856. return document.createTextNode('content')
  857. },
  858. )
  859. },
  860. })
  861. },
  862. }).render()
  863. expect(html()).toBe('content<!--if--><!--slot-->')
  864. toggle.value = false
  865. await nextTick()
  866. expect(html()).toBe('fallback<!--if--><!--slot-->')
  867. toggle.value = true
  868. await nextTick()
  869. expect(html()).toBe('content<!--if--><!--slot-->')
  870. })
  871. test('use fallback on initial render', async () => {
  872. const Child = {
  873. setup() {
  874. return createSlot('default', null, () =>
  875. document.createTextNode('fallback'),
  876. )
  877. },
  878. }
  879. const toggle = ref(false)
  880. const { html } = define({
  881. setup() {
  882. return createComponent(Child, null, {
  883. default: () => {
  884. return createIf(
  885. () => toggle.value,
  886. () => {
  887. return document.createTextNode('content')
  888. },
  889. )
  890. },
  891. })
  892. },
  893. }).render()
  894. expect(html()).toBe('fallback<!--if--><!--slot-->')
  895. toggle.value = true
  896. await nextTick()
  897. expect(html()).toBe('content<!--if--><!--slot-->')
  898. toggle.value = false
  899. await nextTick()
  900. expect(html()).toBe('fallback<!--if--><!--slot-->')
  901. })
  902. test('dynamic slot work with v-if', async () => {
  903. const val = ref('header')
  904. const toggle = ref(false)
  905. const Comp = defineVaporComponent(() => {
  906. const n0 = template('<div></div>')()
  907. prepend(n0 as any as ParentNode, createSlot('header', null))
  908. return n0
  909. })
  910. const { host } = define(() => {
  911. // dynamic slot
  912. return createComponent(Comp, null, {
  913. $: [
  914. () =>
  915. (toggle.value
  916. ? {
  917. name: val.value,
  918. fn: () => {
  919. return template('<h1></h1>')()
  920. },
  921. }
  922. : void 0) as DynamicSlot,
  923. ],
  924. })
  925. }).render()
  926. expect(host.innerHTML).toBe('<div><!--slot--></div>')
  927. toggle.value = true
  928. await nextTick()
  929. expect(host.innerHTML).toBe('<div><h1></h1><!--slot--></div>')
  930. })
  931. test('slots proxy ownKeys trap correctly reflects dynamic slot presence', async () => {
  932. const val = ref('header')
  933. const toggle = ref(false)
  934. let instance: any
  935. const Comp = defineVaporComponent(() => {
  936. instance = currentInstance
  937. const n0 = template('<div></div>')()
  938. prepend(n0 as any as ParentNode, createSlot('header', null))
  939. return n0
  940. })
  941. define(() => {
  942. // dynamic slot
  943. return createComponent(Comp, null, {
  944. $: [
  945. () =>
  946. (toggle.value
  947. ? {
  948. name: val.value,
  949. fn: () => {
  950. return template('<h1></h1>')()
  951. },
  952. }
  953. : void 0) as DynamicSlot,
  954. ],
  955. })
  956. }).render()
  957. expect(Reflect.ownKeys(instance.slots)).not.toContain('header')
  958. toggle.value = true
  959. await nextTick()
  960. expect(Reflect.ownKeys(instance.slots)).toContain('header')
  961. toggle.value = false
  962. await nextTick()
  963. expect(Reflect.ownKeys(instance.slots)).not.toContain('header')
  964. })
  965. test('render fallback when slot content is not valid', async () => {
  966. const Child = {
  967. setup() {
  968. return createSlot('default', null, () =>
  969. document.createTextNode('fallback'),
  970. )
  971. },
  972. }
  973. const { html } = define({
  974. setup() {
  975. return createComponent(Child, null, {
  976. default: () => {
  977. return template('<!--comment-->')()
  978. },
  979. })
  980. },
  981. }).render()
  982. expect(html()).toBe('fallback<!--slot-->')
  983. })
  984. test('render fallback when v-if condition is false', async () => {
  985. const Child = {
  986. setup() {
  987. return createSlot('default', null, () =>
  988. document.createTextNode('fallback'),
  989. )
  990. },
  991. }
  992. const toggle = ref(false)
  993. const { html } = define({
  994. setup() {
  995. return createComponent(Child, null, {
  996. default: () => {
  997. return createIf(
  998. () => toggle.value,
  999. () => {
  1000. return document.createTextNode('content')
  1001. },
  1002. )
  1003. },
  1004. })
  1005. },
  1006. }).render()
  1007. expect(html()).toBe('fallback<!--if--><!--slot-->')
  1008. toggle.value = true
  1009. await nextTick()
  1010. expect(html()).toBe('content<!--if--><!--slot-->')
  1011. toggle.value = false
  1012. await nextTick()
  1013. expect(html()).toBe('fallback<!--if--><!--slot-->')
  1014. })
  1015. test('render fallback with nested v-if', async () => {
  1016. const Child = {
  1017. setup() {
  1018. return createSlot('default', null, () =>
  1019. document.createTextNode('fallback'),
  1020. )
  1021. },
  1022. }
  1023. const outerShow = ref(false)
  1024. const innerShow = ref(false)
  1025. const { html } = define({
  1026. setup() {
  1027. return createComponent(Child, null, {
  1028. default: () => {
  1029. return createIf(
  1030. () => outerShow.value,
  1031. () => {
  1032. return createIf(
  1033. () => innerShow.value,
  1034. () => {
  1035. return document.createTextNode('content')
  1036. },
  1037. )
  1038. },
  1039. )
  1040. },
  1041. })
  1042. },
  1043. }).render()
  1044. expect(html()).toBe('fallback<!--if--><!--slot-->')
  1045. outerShow.value = true
  1046. await nextTick()
  1047. expect(html()).toBe('fallback<!--if--><!--if--><!--slot-->')
  1048. innerShow.value = true
  1049. await nextTick()
  1050. expect(html()).toBe('content<!--if--><!--if--><!--slot-->')
  1051. innerShow.value = false
  1052. await nextTick()
  1053. expect(html()).toBe('fallback<!--if--><!--if--><!--slot-->')
  1054. outerShow.value = false
  1055. await nextTick()
  1056. expect(html()).toBe('fallback<!--if--><!--slot-->')
  1057. outerShow.value = true
  1058. await nextTick()
  1059. expect(html()).toBe('fallback<!--if--><!--if--><!--slot-->')
  1060. innerShow.value = true
  1061. await nextTick()
  1062. expect(html()).toBe('content<!--if--><!--if--><!--slot-->')
  1063. })
  1064. test('render fallback with v-for', async () => {
  1065. const Child = {
  1066. setup() {
  1067. return createSlot('default', null, () =>
  1068. document.createTextNode('fallback'),
  1069. )
  1070. },
  1071. }
  1072. const items = ref<number[]>([1])
  1073. const { html } = define({
  1074. setup() {
  1075. return createComponent(Child, null, {
  1076. default: () => {
  1077. const n2 = createFor(
  1078. () => items.value,
  1079. for_item0 => {
  1080. const n4 = template('<span> </span>')() as any
  1081. const x4 = child(n4) as any
  1082. renderEffect(() =>
  1083. setText(x4, toDisplayString(for_item0.value)),
  1084. )
  1085. return n4
  1086. },
  1087. )
  1088. return n2
  1089. },
  1090. })
  1091. },
  1092. }).render()
  1093. expect(html()).toBe('<span>1</span><!--for--><!--slot-->')
  1094. items.value.pop()
  1095. await nextTick()
  1096. expect(html()).toBe('fallback<!--for--><!--slot-->')
  1097. items.value.pop()
  1098. await nextTick()
  1099. expect(html()).toBe('fallback<!--for--><!--slot-->')
  1100. items.value.push(2)
  1101. await nextTick()
  1102. expect(html()).toBe('<span>2</span><!--for--><!--slot-->')
  1103. })
  1104. test('render fallback with v-for (empty source)', async () => {
  1105. const Child = {
  1106. setup() {
  1107. return createSlot('default', null, () =>
  1108. document.createTextNode('fallback'),
  1109. )
  1110. },
  1111. }
  1112. const items = ref<number[]>([])
  1113. const { html } = define({
  1114. setup() {
  1115. return createComponent(Child, null, {
  1116. default: () => {
  1117. const n2 = createFor(
  1118. () => items.value,
  1119. for_item0 => {
  1120. const n4 = template('<span> </span>')() as any
  1121. const x4 = child(n4) as any
  1122. renderEffect(() =>
  1123. setText(x4, toDisplayString(for_item0.value)),
  1124. )
  1125. return n4
  1126. },
  1127. )
  1128. return n2
  1129. },
  1130. })
  1131. },
  1132. }).render()
  1133. expect(html()).toBe('fallback<!--for--><!--slot-->')
  1134. items.value.push(1)
  1135. await nextTick()
  1136. expect(html()).toBe('<span>1</span><!--for--><!--slot-->')
  1137. items.value.pop()
  1138. await nextTick()
  1139. expect(html()).toBe('fallback<!--for--><!--slot-->')
  1140. items.value.pop()
  1141. await nextTick()
  1142. expect(html()).toBe('fallback<!--for--><!--slot-->')
  1143. items.value.push(2)
  1144. await nextTick()
  1145. expect(html()).toBe('<span>2</span><!--for--><!--slot-->')
  1146. })
  1147. test('render fallback with invalid v-for branch', async () => {
  1148. const Child = {
  1149. setup() {
  1150. return createSlot('default', null, () =>
  1151. document.createTextNode('fallback'),
  1152. )
  1153. },
  1154. }
  1155. const items = ref([{ text: 'bar', show: false }])
  1156. const { html } = define({
  1157. setup() {
  1158. return createComponent(Child, null, {
  1159. default: () => {
  1160. return createFor(
  1161. () => items.value,
  1162. for_item0 => {
  1163. return createIf(
  1164. () => for_item0.value.show,
  1165. () => {
  1166. const n5 = template('<span> </span>')() as any
  1167. const x5 = child(n5) as any
  1168. renderEffect(() =>
  1169. setText(x5, toDisplayString(for_item0.value.text)),
  1170. )
  1171. return n5
  1172. },
  1173. )
  1174. },
  1175. item => item.text,
  1176. )
  1177. },
  1178. })
  1179. },
  1180. }).render()
  1181. expect(html()).toBe('fallback<!--if--><!--for--><!--slot-->')
  1182. items.value[0].show = true
  1183. await nextTick()
  1184. expect(html()).toBe('<span>bar</span><!--if--><!--for--><!--slot-->')
  1185. items.value[0].show = false
  1186. await nextTick()
  1187. expect(html()).toBe('fallback<!--if--><!--for--><!--slot-->')
  1188. })
  1189. test('should not render fallback for a single empty item in v-for', async () => {
  1190. const Child = {
  1191. setup() {
  1192. return createSlot('default', null, () =>
  1193. document.createTextNode('fallback'),
  1194. )
  1195. },
  1196. }
  1197. const items = ref([
  1198. { text: 'bar', show: true },
  1199. { text: 'baz', show: true },
  1200. ])
  1201. const { html } = define({
  1202. setup() {
  1203. return createComponent(Child, null, {
  1204. default: () => {
  1205. return createFor(
  1206. () => items.value,
  1207. for_item0 => {
  1208. return createIf(
  1209. () => for_item0.value.show,
  1210. () => {
  1211. const n5 = template('<span> </span>')() as any
  1212. const x5 = child(n5) as any
  1213. renderEffect(() =>
  1214. setText(x5, toDisplayString(for_item0.value.text)),
  1215. )
  1216. return n5
  1217. },
  1218. )
  1219. },
  1220. item => item.text,
  1221. )
  1222. },
  1223. })
  1224. },
  1225. }).render()
  1226. expect(html()).toBe(
  1227. '<span>bar</span><!--if--><span>baz</span><!--if--><!--for--><!--slot-->',
  1228. )
  1229. items.value[1].show = false
  1230. await nextTick()
  1231. expect(html()).toBe(
  1232. '<span>bar</span><!--if--><!--if--><!--for--><!--slot-->',
  1233. )
  1234. })
  1235. test('work with v-once', async () => {
  1236. const Child = defineVaporComponent({
  1237. setup() {
  1238. return createSlot(
  1239. 'default',
  1240. null,
  1241. undefined,
  1242. undefined,
  1243. true /* once */,
  1244. )
  1245. },
  1246. })
  1247. const count = ref(0)
  1248. const { html } = define({
  1249. setup() {
  1250. return createComponent(Child, null, {
  1251. default: () => {
  1252. const n3 = template('<div> </div>')() as any
  1253. const x3 = txt(n3) as any
  1254. renderEffect(() => setText(x3, toDisplayString(count.value)))
  1255. return n3
  1256. },
  1257. })
  1258. },
  1259. }).render()
  1260. expect(html()).toBe('<div>0</div><!--slot-->')
  1261. // expect no changes due to v-once
  1262. count.value++
  1263. await nextTick()
  1264. expect(html()).toBe('<div>0</div><!--slot-->')
  1265. })
  1266. })
  1267. describe('forwarded slot', () => {
  1268. test('should work', async () => {
  1269. const Child = defineVaporComponent({
  1270. setup() {
  1271. return createSlot('foo', null)
  1272. },
  1273. })
  1274. const Parent = defineVaporComponent({
  1275. setup() {
  1276. const n2 = createComponent(
  1277. Child,
  1278. null,
  1279. {
  1280. foo: withVaporCtx(() => {
  1281. return createSlot('foo', null)
  1282. }),
  1283. },
  1284. true,
  1285. )
  1286. return n2
  1287. },
  1288. })
  1289. const foo = ref('foo')
  1290. const { host } = define({
  1291. setup() {
  1292. const n2 = createComponent(
  1293. Parent,
  1294. null,
  1295. {
  1296. foo: () => {
  1297. const n0 = template(' ')() as any
  1298. renderEffect(() => setText(n0, foo.value))
  1299. return n0
  1300. },
  1301. },
  1302. true,
  1303. )
  1304. return n2
  1305. },
  1306. }).render()
  1307. expect(host.innerHTML).toBe('foo<!--slot--><!--slot-->')
  1308. foo.value = 'bar'
  1309. await nextTick()
  1310. expect(host.innerHTML).toBe('bar<!--slot--><!--slot-->')
  1311. })
  1312. test('mixed with non-forwarded slot', async () => {
  1313. const Child = defineVaporComponent({
  1314. setup() {
  1315. return [createSlot('foo', null)]
  1316. },
  1317. })
  1318. const Parent = defineVaporComponent({
  1319. setup() {
  1320. const n2 = createComponent(Child, null, {
  1321. foo: withVaporCtx(() => {
  1322. const n0 = createSlot('foo', null)
  1323. return n0
  1324. }),
  1325. })
  1326. const n3 = createSlot('default', null)
  1327. return [n2, n3]
  1328. },
  1329. })
  1330. const foo = ref('foo')
  1331. const { host } = define({
  1332. setup() {
  1333. const n2 = createComponent(
  1334. Parent,
  1335. null,
  1336. {
  1337. foo: () => {
  1338. const n0 = template(' ')() as any
  1339. renderEffect(() => setText(n0, foo.value))
  1340. return n0
  1341. },
  1342. default: () => {
  1343. const n3 = template(' ')() as any
  1344. renderEffect(() => setText(n3, foo.value))
  1345. return n3
  1346. },
  1347. },
  1348. true,
  1349. )
  1350. return n2
  1351. },
  1352. }).render()
  1353. expect(host.innerHTML).toBe('foo<!--slot--><!--slot-->foo<!--slot-->')
  1354. foo.value = 'bar'
  1355. await nextTick()
  1356. expect(host.innerHTML).toBe('bar<!--slot--><!--slot-->bar<!--slot-->')
  1357. })
  1358. test('forwarded slot with fallback', async () => {
  1359. const Child = defineVaporComponent({
  1360. setup() {
  1361. return createSlot('default', null, () => template('child fallback')())
  1362. },
  1363. })
  1364. const Parent = defineVaporComponent({
  1365. setup() {
  1366. const n2 = createComponent(Child, null, {
  1367. default: withVaporCtx(() => {
  1368. const n0 = createSlot('default', null, () => {
  1369. return template('<!-- <div></div> -->')()
  1370. })
  1371. return n0
  1372. }),
  1373. })
  1374. return n2
  1375. },
  1376. })
  1377. const { html } = define({
  1378. setup() {
  1379. return createComponent(Parent, null, {
  1380. default: () => template('<!-- <div>App</div> -->')(),
  1381. })
  1382. },
  1383. }).render()
  1384. expect(html()).toBe('child fallback<!--slot--><!--slot-->')
  1385. })
  1386. test('named forwarded slot with v-if', async () => {
  1387. const Child = defineVaporComponent({
  1388. setup() {
  1389. return createSlot('default', null)
  1390. },
  1391. })
  1392. const Parent = defineVaporComponent({
  1393. props: {
  1394. show: Boolean,
  1395. },
  1396. setup(props) {
  1397. const n6 = createComponent(
  1398. Child,
  1399. null,
  1400. {
  1401. default: withVaporCtx(() => {
  1402. const n0 = createIf(
  1403. () => props.show,
  1404. () => {
  1405. const n5 = template('<div></div>')() as any
  1406. setInsertionState(n5, null, 0, true)
  1407. createSlot('header', null, () => {
  1408. const n4 = template('default header')()
  1409. return n4
  1410. })
  1411. return n5
  1412. },
  1413. )
  1414. return n0
  1415. }),
  1416. },
  1417. true,
  1418. )
  1419. return n6
  1420. },
  1421. })
  1422. const show = ref(false)
  1423. const { html } = define({
  1424. setup() {
  1425. return createComponent(
  1426. Parent,
  1427. {
  1428. show: () => show.value,
  1429. },
  1430. {
  1431. header: () => template('custom header')(),
  1432. },
  1433. )
  1434. },
  1435. }).render()
  1436. expect(html()).toBe('<!--if--><!--slot-->')
  1437. show.value = true
  1438. await nextTick()
  1439. expect(html()).toBe(
  1440. '<div>custom header<!--slot--></div><!--if--><!--slot-->',
  1441. )
  1442. show.value = false
  1443. await nextTick()
  1444. expect(html()).toBe('<!--if--><!--slot-->')
  1445. })
  1446. test('forwarded slot with fallback (v-if)', async () => {
  1447. const Child = defineVaporComponent({
  1448. setup() {
  1449. return createSlot('default', null, () => template('child fallback')())
  1450. },
  1451. })
  1452. const show = ref(false)
  1453. const Parent = defineVaporComponent({
  1454. setup() {
  1455. const n2 = createComponent(Child, null, {
  1456. default: withVaporCtx(() => {
  1457. const n0 = createSlot('default', null, () => {
  1458. const n2 = createIf(
  1459. () => show.value,
  1460. () => {
  1461. const n4 = template('<div>if content</div>')()
  1462. return n4
  1463. },
  1464. )
  1465. return n2
  1466. })
  1467. return n0
  1468. }),
  1469. })
  1470. return n2
  1471. },
  1472. })
  1473. const { html } = define({
  1474. setup() {
  1475. return createComponent(Parent, null, {
  1476. default: () => template('<!-- <div>App</div> -->')(),
  1477. })
  1478. },
  1479. }).render()
  1480. expect(html()).toBe('child fallback<!--if--><!--slot--><!--slot-->')
  1481. show.value = true
  1482. await nextTick()
  1483. expect(html()).toBe(
  1484. '<div>if content</div><!--if--><!--slot--><!--slot-->',
  1485. )
  1486. })
  1487. test('forwarded slot with fallback (v-for)', async () => {
  1488. const Child = defineVaporComponent({
  1489. setup() {
  1490. return createSlot('default', null, () => template('child fallback')())
  1491. },
  1492. })
  1493. const items = ref<number[]>([])
  1494. const Parent = defineVaporComponent({
  1495. setup() {
  1496. const n2 = createComponent(Child, null, {
  1497. default: withVaporCtx(() => {
  1498. const n0 = createSlot('default', null, () => {
  1499. const n2 = createFor(
  1500. () => items.value,
  1501. for_item0 => {
  1502. const n4 = template('<span> </span>')() as any
  1503. const x4 = child(n4) as any
  1504. renderEffect(() =>
  1505. setText(x4, toDisplayString(for_item0.value)),
  1506. )
  1507. return n4
  1508. },
  1509. )
  1510. return n2
  1511. })
  1512. return n0
  1513. }),
  1514. })
  1515. return n2
  1516. },
  1517. })
  1518. const { html } = define({
  1519. setup() {
  1520. return createComponent(Parent, null, {
  1521. default: () => template('<!-- <div>App</div> -->')(),
  1522. })
  1523. },
  1524. }).render()
  1525. expect(html()).toBe('child fallback<!--for--><!--slot--><!--slot-->')
  1526. items.value.push(1)
  1527. await nextTick()
  1528. expect(html()).toBe('<span>1</span><!--for--><!--slot--><!--slot-->')
  1529. items.value.pop()
  1530. await nextTick()
  1531. expect(html()).toBe('child fallback<!--for--><!--slot--><!--slot-->')
  1532. })
  1533. test('consecutive slots with insertion state', async () => {
  1534. const { component: Child } = define({
  1535. setup() {
  1536. const n2 = template('<div><div>baz</div></div>', true)() as any
  1537. setInsertionState(n2, 0)
  1538. createSlot('default', null)
  1539. setInsertionState(n2, 0)
  1540. createSlot('foo', null)
  1541. return n2
  1542. },
  1543. })
  1544. const { html } = define({
  1545. setup() {
  1546. return createComponent(Child, null, {
  1547. default: () => template('default')(),
  1548. foo: () => template('foo')(),
  1549. })
  1550. },
  1551. }).render()
  1552. expect(html()).toBe(
  1553. `<div>` +
  1554. `default<!--slot-->` +
  1555. `foo<!--slot-->` +
  1556. `<div>baz</div>` +
  1557. `</div>`,
  1558. )
  1559. })
  1560. describe('vdom interop', () => {
  1561. const createVaporSlot = (fallbackText = 'fallback') => {
  1562. return defineVaporComponent({
  1563. setup() {
  1564. const n0 = createSlot('foo', null, () => {
  1565. const n2 = template(`<div>${fallbackText}</div>`)()
  1566. return n2
  1567. })
  1568. return n0
  1569. },
  1570. })
  1571. }
  1572. const createVdomSlot = (fallbackText = 'fallback') => {
  1573. return {
  1574. render(this: any) {
  1575. return renderSlot(this.$slots, 'foo', {}, () => [
  1576. h('div', fallbackText),
  1577. ])
  1578. },
  1579. }
  1580. }
  1581. const createVaporForwardedSlot = (
  1582. targetComponent: any,
  1583. fallbackText?: string,
  1584. ) => {
  1585. return defineVaporComponent({
  1586. setup() {
  1587. const n2 = createComponent(
  1588. targetComponent,
  1589. null,
  1590. {
  1591. foo: withVaporCtx(() => {
  1592. return fallbackText
  1593. ? createSlot('foo', null, () => {
  1594. const n2 = template(`<div>${fallbackText}</div>`)()
  1595. return n2
  1596. })
  1597. : createSlot('foo', null)
  1598. }),
  1599. },
  1600. true,
  1601. )
  1602. return n2
  1603. },
  1604. })
  1605. }
  1606. const createVdomForwardedSlot = (
  1607. targetComponent: any,
  1608. fallbackText?: string,
  1609. ) => {
  1610. return {
  1611. render(this: any) {
  1612. return h(targetComponent, null, {
  1613. foo: () => [
  1614. fallbackText
  1615. ? renderSlot(this.$slots, 'foo', {}, () => [
  1616. h('div', fallbackText),
  1617. ])
  1618. : renderSlot(this.$slots, 'foo'),
  1619. ],
  1620. _: 3 /* FORWARDED */,
  1621. })
  1622. },
  1623. }
  1624. }
  1625. const createMultipleVaporForwardedSlots = (
  1626. targetComponent: any,
  1627. count: number,
  1628. ) => {
  1629. let current = targetComponent
  1630. for (let i = 0; i < count; i++) {
  1631. current = createVaporForwardedSlot(current)
  1632. }
  1633. return current
  1634. }
  1635. const createMultipleVdomForwardedSlots = (
  1636. targetComponent: any,
  1637. count: number,
  1638. ) => {
  1639. let current = targetComponent
  1640. for (let i = 0; i < count; i++) {
  1641. current = createVdomForwardedSlot(current)
  1642. }
  1643. return current
  1644. }
  1645. const createTestApp = (
  1646. rootComponent: any,
  1647. foo: Ref<string>,
  1648. show: Ref<boolean>,
  1649. ) => {
  1650. return {
  1651. setup() {
  1652. return () =>
  1653. h(
  1654. rootComponent,
  1655. null,
  1656. createSlots({ _: 2 /* DYNAMIC */ } as any, [
  1657. show.value
  1658. ? {
  1659. name: 'foo',
  1660. fn: () => [h('span', foo.value)],
  1661. key: '0',
  1662. }
  1663. : undefined,
  1664. ]),
  1665. )
  1666. },
  1667. }
  1668. }
  1669. const createEmptyTestApp = (rootComponent: any) => {
  1670. return {
  1671. setup() {
  1672. return () => h(rootComponent)
  1673. },
  1674. }
  1675. }
  1676. test('vdom slot > vapor forwarded slot > vapor slot', async () => {
  1677. const foo = ref('foo')
  1678. const show = ref(true)
  1679. const VaporSlot = createVaporSlot()
  1680. const VaporForwardedSlot = createVaporForwardedSlot(VaporSlot)
  1681. const App = createTestApp(VaporForwardedSlot, foo, show)
  1682. const root = document.createElement('div')
  1683. createApp(App).use(vaporInteropPlugin).mount(root)
  1684. expect(root.innerHTML).toBe('<span>foo</span><!--slot-->')
  1685. foo.value = 'bar'
  1686. await nextTick()
  1687. expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
  1688. show.value = false
  1689. await nextTick()
  1690. expect(root.innerHTML).toBe('<div>fallback</div><!--slot-->')
  1691. })
  1692. test('vdom slot > vapor forwarded slot(with fallback) > vapor slot', async () => {
  1693. const foo = ref('foo')
  1694. const show = ref(true)
  1695. const VaporSlot = createVaporSlot()
  1696. const VaporForwardedSlotWithFallback = createVaporForwardedSlot(
  1697. VaporSlot,
  1698. 'forwarded fallback',
  1699. )
  1700. const App = createTestApp(VaporForwardedSlotWithFallback, foo, show)
  1701. const root = document.createElement('div')
  1702. createApp(App).use(vaporInteropPlugin).mount(root)
  1703. expect(root.innerHTML).toBe('<span>foo</span><!--slot-->')
  1704. foo.value = 'bar'
  1705. await nextTick()
  1706. expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
  1707. show.value = false
  1708. await nextTick()
  1709. expect(root.innerHTML).toBe('<div>forwarded fallback</div><!--slot-->')
  1710. })
  1711. test('vdom slot > vapor forwarded slot > vdom slot', async () => {
  1712. const foo = ref('foo')
  1713. const show = ref(true)
  1714. const VdomSlot = createVdomSlot()
  1715. const VaporForwardedSlot = createVaporForwardedSlot(VdomSlot)
  1716. const App = createTestApp(VaporForwardedSlot, foo, show)
  1717. const root = document.createElement('div')
  1718. createApp(App).use(vaporInteropPlugin).mount(root)
  1719. expect(root.innerHTML).toBe('<span>foo</span>')
  1720. foo.value = 'bar'
  1721. await nextTick()
  1722. expect(root.innerHTML).toBe('<span>bar</span>')
  1723. show.value = false
  1724. await nextTick()
  1725. expect(root.innerHTML).toBe('<div>fallback</div>')
  1726. show.value = true
  1727. await nextTick()
  1728. expect(root.innerHTML).toBe('<span>bar</span>')
  1729. })
  1730. test('vdom slot > vapor forwarded slot(with fallback) > vdom slot', async () => {
  1731. const foo = ref('foo')
  1732. const show = ref(true)
  1733. const VdomSlot = createVdomSlot()
  1734. const VaporForwardedSlotWithFallback = createVaporForwardedSlot(
  1735. VdomSlot,
  1736. 'forwarded fallback',
  1737. )
  1738. const App = createTestApp(VaporForwardedSlotWithFallback, foo, show)
  1739. const root = document.createElement('div')
  1740. createApp(App).use(vaporInteropPlugin).mount(root)
  1741. expect(root.innerHTML).toBe('<span>foo</span>')
  1742. foo.value = 'bar'
  1743. await nextTick()
  1744. expect(root.innerHTML).toBe('<span>bar</span>')
  1745. show.value = false
  1746. await nextTick()
  1747. expect(root.innerHTML).toBe('<div>forwarded fallback</div>')
  1748. })
  1749. test('vdom slot > vapor forwarded slot > vdom forwarded slot > vapor slot', async () => {
  1750. const foo = ref('foo')
  1751. const show = ref(true)
  1752. const VaporSlot = createVaporSlot()
  1753. const VdomForwardedSlot = createVdomForwardedSlot(VaporSlot)
  1754. const VaporForwardedSlot = createVaporForwardedSlot(VdomForwardedSlot)
  1755. const App = createTestApp(VaporForwardedSlot, foo, show)
  1756. const root = document.createElement('div')
  1757. createApp(App).use(vaporInteropPlugin).mount(root)
  1758. expect(root.innerHTML).toBe('<span>foo</span>')
  1759. foo.value = 'bar'
  1760. await nextTick()
  1761. expect(root.innerHTML).toBe('<span>bar</span>')
  1762. show.value = false
  1763. await nextTick()
  1764. expect(root.innerHTML).toBe('<div>fallback</div>')
  1765. show.value = true
  1766. await nextTick()
  1767. expect(root.innerHTML).toBe('<span>bar</span>')
  1768. })
  1769. test('vdom slot > vapor forwarded slot(with fallback) > vdom forwarded slot > vapor slot', async () => {
  1770. const foo = ref('foo')
  1771. const show = ref(true)
  1772. const VaporSlot = createVaporSlot()
  1773. const VdomForwardedSlot = createVdomForwardedSlot(VaporSlot)
  1774. const VaporForwardedSlotWithFallback = createVaporForwardedSlot(
  1775. VdomForwardedSlot,
  1776. 'forwarded fallback',
  1777. )
  1778. const App = createTestApp(VaporForwardedSlotWithFallback, foo, show)
  1779. const root = document.createElement('div')
  1780. createApp(App).use(vaporInteropPlugin).mount(root)
  1781. expect(root.innerHTML).toBe('<span>foo</span>')
  1782. foo.value = 'bar'
  1783. await nextTick()
  1784. expect(root.innerHTML).toBe('<span>bar</span>')
  1785. show.value = false
  1786. await nextTick()
  1787. expect(root.innerHTML).toBe('<div>forwarded fallback</div>')
  1788. show.value = true
  1789. await nextTick()
  1790. expect(root.innerHTML).toBe('<span>bar</span>')
  1791. })
  1792. test('vdom slot > vapor forwarded slot > vdom forwarded slot(with fallback) > vapor slot', async () => {
  1793. const foo = ref('foo')
  1794. const show = ref(true)
  1795. const VaporSlot = createVaporSlot()
  1796. const VdomForwardedSlotWithFallback = createVdomForwardedSlot(
  1797. VaporSlot,
  1798. 'vdom fallback',
  1799. )
  1800. const VaporForwardedSlot = createVaporForwardedSlot(
  1801. VdomForwardedSlotWithFallback,
  1802. )
  1803. const App = createTestApp(VaporForwardedSlot, foo, show)
  1804. const root = document.createElement('div')
  1805. createApp(App).use(vaporInteropPlugin).mount(root)
  1806. expect(root.innerHTML).toBe('<span>foo</span>')
  1807. foo.value = 'bar'
  1808. await nextTick()
  1809. expect(root.innerHTML).toBe('<span>bar</span>')
  1810. show.value = false
  1811. await nextTick()
  1812. expect(root.innerHTML).toBe('<div>vdom fallback</div>')
  1813. show.value = true
  1814. await nextTick()
  1815. expect(root.innerHTML).toBe('<span>bar</span>')
  1816. })
  1817. test('vdom slot(empty) > vapor forwarded slot > vdom forwarded slot(with fallback) > vapor slot', async () => {
  1818. const VaporSlot = createVaporSlot()
  1819. const VdomForwardedSlotWithFallback = createVdomForwardedSlot(
  1820. VaporSlot,
  1821. 'vdom fallback',
  1822. )
  1823. const VaporForwardedSlot = createVaporForwardedSlot(
  1824. VdomForwardedSlotWithFallback,
  1825. )
  1826. const App = createEmptyTestApp(VaporForwardedSlot)
  1827. const root = document.createElement('div')
  1828. createApp(App).use(vaporInteropPlugin).mount(root)
  1829. expect(root.innerHTML).toBe('<div>vdom fallback</div>')
  1830. })
  1831. test('vdom forwarded fallback updates existing vapor slot block', async () => {
  1832. const fallbackText = ref('fallback')
  1833. const VdomForwardedSlotWithReactiveFallback = {
  1834. render(this: any) {
  1835. return renderSlot(this.$slots, 'foo', {}, () => [
  1836. h('div', fallbackText.value),
  1837. ])
  1838. },
  1839. }
  1840. const VaporForwardedSlot = createVaporForwardedSlot(
  1841. VdomForwardedSlotWithReactiveFallback,
  1842. )
  1843. const App = createEmptyTestApp(VaporForwardedSlot)
  1844. const root = document.createElement('div')
  1845. createApp(App).use(vaporInteropPlugin).mount(root)
  1846. expect(root.innerHTML).toBe('<div>fallback</div>')
  1847. fallbackText.value = 'updated'
  1848. await nextTick()
  1849. expect(root.innerHTML).toBe('<div>updated</div>')
  1850. })
  1851. test('vdom forwarded fallback updates when fallback function identity changes', async () => {
  1852. const useAlt = ref(false)
  1853. const VdomForwardedSlotWithDynamicFallback = {
  1854. render(this: any) {
  1855. const tag = useAlt.value ? 'p' : 'div'
  1856. const text = useAlt.value ? 'alt fallback' : 'fallback'
  1857. return renderSlot(this.$slots, 'foo', {}, () => [h(tag, text)])
  1858. },
  1859. }
  1860. const VaporForwardedSlot = createVaporForwardedSlot(
  1861. VdomForwardedSlotWithDynamicFallback,
  1862. )
  1863. const App = createEmptyTestApp(VaporForwardedSlot)
  1864. const root = document.createElement('div')
  1865. createApp(App).use(vaporInteropPlugin).mount(root)
  1866. expect(root.innerHTML).toBe('<div>fallback</div>')
  1867. useAlt.value = true
  1868. await nextTick()
  1869. expect(root.innerHTML).toBe('<p>alt fallback</p>')
  1870. })
  1871. test('vdom forwarded fallback identity switch disposes stale fallback effects', async () => {
  1872. const useAlt = ref(false)
  1873. const primaryText = ref('fallback')
  1874. const altText = ref('alt fallback')
  1875. const VdomForwardedSlotWithDynamicFallback = {
  1876. render(this: any) {
  1877. return renderSlot(
  1878. this.$slots,
  1879. 'foo',
  1880. {},
  1881. useAlt.value
  1882. ? () => [h('p', altText.value)]
  1883. : () => [h('div', primaryText.value)],
  1884. )
  1885. },
  1886. }
  1887. const VaporForwardedSlot = createVaporForwardedSlot(
  1888. VdomForwardedSlotWithDynamicFallback,
  1889. )
  1890. const App = createEmptyTestApp(VaporForwardedSlot)
  1891. const root = document.createElement('div')
  1892. createApp(App).use(vaporInteropPlugin).mount(root)
  1893. expect(root.innerHTML).toBe('<div>fallback</div>')
  1894. useAlt.value = true
  1895. await nextTick()
  1896. expect(root.innerHTML).toBe('<p>alt fallback</p>')
  1897. primaryText.value = 'stale fallback'
  1898. await nextTick()
  1899. expect(root.innerHTML).toBe('<p>alt fallback</p>')
  1900. altText.value = 'active fallback'
  1901. await nextTick()
  1902. expect(root.innerHTML).toBe('<p>active fallback</p>')
  1903. })
  1904. test('vdom fallback updates when renderVaporSlot returns slot fragment', async () => {
  1905. const useAlt = ref(false)
  1906. const VdomSlotWithDynamicFallback = {
  1907. render(this: any) {
  1908. const tag = useAlt.value ? 'p' : 'div'
  1909. const text = useAlt.value ? 'alt fallback' : 'fallback'
  1910. return renderSlot(this.$slots, 'foo', {}, () => [h(tag, text)])
  1911. },
  1912. }
  1913. const VaporForwardedSlot = defineVaporComponent({
  1914. setup() {
  1915. return createComponent(
  1916. VdomSlotWithDynamicFallback,
  1917. null,
  1918. {
  1919. foo: withVaporCtx(() => createSlot('foo', null)),
  1920. },
  1921. true,
  1922. )
  1923. },
  1924. })
  1925. const root = document.createElement('div')
  1926. createVaporApp(VaporForwardedSlot).use(vaporInteropPlugin).mount(root)
  1927. expect(root.innerHTML).toBe('<div>fallback</div><!--slot-->')
  1928. useAlt.value = true
  1929. await nextTick()
  1930. expect(root.innerHTML).toBe('<p>alt fallback</p><!--slot-->')
  1931. })
  1932. test('vdom fallback for renderVaporSlot is evaluated once on initial mount', async () => {
  1933. const fallbackText = ref('fallback')
  1934. const fallbackSpy = vi.fn(() => [h('div', fallbackText.value)])
  1935. const VdomSlotWithCountingFallback = {
  1936. render(this: any) {
  1937. return renderSlot(this.$slots, 'foo', {}, fallbackSpy)
  1938. },
  1939. }
  1940. const VaporForwardedSlot = defineVaporComponent({
  1941. setup() {
  1942. return createComponent(
  1943. VdomSlotWithCountingFallback,
  1944. null,
  1945. {
  1946. foo: withVaporCtx(() => createSlot('foo', null)),
  1947. },
  1948. true,
  1949. )
  1950. },
  1951. })
  1952. const root = document.createElement('div')
  1953. createVaporApp(VaporForwardedSlot).use(vaporInteropPlugin).mount(root)
  1954. expect(root.innerHTML).toBe('<div>fallback</div><!--slot-->')
  1955. expect(fallbackSpy).toHaveBeenCalledTimes(1)
  1956. fallbackText.value = 'updated fallback'
  1957. await nextTick()
  1958. expect(root.innerHTML).toBe('<div>updated fallback</div><!--slot-->')
  1959. expect(fallbackSpy).toHaveBeenCalledTimes(2)
  1960. })
  1961. test('vdom fallback for renderVaporSlot supports text children', async () => {
  1962. const fallbackText = ref('fallback')
  1963. const fallbackSpy = vi.fn(() => [fallbackText.value])
  1964. const VdomSlotWithTextFallback = {
  1965. render(this: any) {
  1966. return renderSlot(this.$slots, 'foo', {}, fallbackSpy)
  1967. },
  1968. }
  1969. const VaporForwardedSlot = defineVaporComponent({
  1970. setup() {
  1971. return createComponent(
  1972. VdomSlotWithTextFallback,
  1973. null,
  1974. {
  1975. foo: withVaporCtx(() => createSlot('foo', null)),
  1976. },
  1977. true,
  1978. )
  1979. },
  1980. })
  1981. const root = document.createElement('div')
  1982. createVaporApp(VaporForwardedSlot).use(vaporInteropPlugin).mount(root)
  1983. expect(root.innerHTML).toBe('fallback<!--slot-->')
  1984. expect(fallbackSpy).toHaveBeenCalledTimes(1)
  1985. fallbackText.value = 'updated fallback'
  1986. await nextTick()
  1987. expect(root.innerHTML).toBe('updated fallback<!--slot-->')
  1988. expect(fallbackSpy).toHaveBeenCalledTimes(2)
  1989. })
  1990. test('moving active vdom fallback keeps slot carrier order after teleport move', async () => {
  1991. const targetA = document.createElement('div')
  1992. targetA.id = 'component-slots-fallback-target-a'
  1993. const targetB = document.createElement('div')
  1994. targetB.id = 'component-slots-fallback-target-b'
  1995. document.body.append(targetA, targetB)
  1996. const to = ref('#component-slots-fallback-target-a')
  1997. const fallbackText = ref('fallback')
  1998. try {
  1999. const VdomSlotWithReactiveFallback = {
  2000. render(this: any) {
  2001. return renderSlot(this.$slots, 'foo', {}, () => [
  2002. h('div', fallbackText.value),
  2003. ])
  2004. },
  2005. }
  2006. const VaporTeleportedSlot = defineVaporComponent({
  2007. setup() {
  2008. return createComponent(
  2009. VaporTeleport,
  2010. {
  2011. to: () => to.value,
  2012. },
  2013. {
  2014. default: withVaporCtx(() =>
  2015. createComponent(
  2016. VdomSlotWithReactiveFallback,
  2017. null,
  2018. {
  2019. foo: withVaporCtx(() => createSlot('foo', null)),
  2020. },
  2021. true,
  2022. ),
  2023. ),
  2024. },
  2025. )
  2026. },
  2027. })
  2028. const host = document.createElement('div')
  2029. const app = createVaporApp(VaporTeleportedSlot)
  2030. app.use(vaporInteropPlugin)
  2031. app.mount(host)
  2032. await nextTick()
  2033. expect(targetA.innerHTML).toBe('<div>fallback</div><!--slot-->')
  2034. expect(targetB.innerHTML).toBe('')
  2035. to.value = '#component-slots-fallback-target-b'
  2036. await nextTick()
  2037. expect(targetA.innerHTML).toBe('')
  2038. expect(targetB.innerHTML).toBe('<div>fallback</div><!--slot-->')
  2039. fallbackText.value = 'moved fallback'
  2040. await nextTick()
  2041. expect(targetB.innerHTML).toBe('<div>moved fallback</div><!--slot-->')
  2042. app.unmount()
  2043. } finally {
  2044. targetA.remove()
  2045. targetB.remove()
  2046. }
  2047. })
  2048. test('vdom fallback removal clears inherited vapor fallback', async () => {
  2049. const useFallback = ref(true)
  2050. const VdomSlotWithOptionalFallback = {
  2051. render(this: any) {
  2052. return renderSlot(
  2053. this.$slots,
  2054. 'foo',
  2055. {},
  2056. useFallback.value ? () => [h('div', 'fallback')] : undefined,
  2057. )
  2058. },
  2059. }
  2060. const VaporForwardedSlot = defineVaporComponent({
  2061. setup() {
  2062. return createComponent(
  2063. VdomSlotWithOptionalFallback,
  2064. null,
  2065. {
  2066. foo: withVaporCtx(() => createSlot('foo', null)),
  2067. },
  2068. true,
  2069. )
  2070. },
  2071. })
  2072. const root = document.createElement('div')
  2073. createVaporApp(VaporForwardedSlot).use(vaporInteropPlugin).mount(root)
  2074. expect(root.innerHTML).toBe('<div>fallback</div><!--slot-->')
  2075. useFallback.value = false
  2076. await nextTick()
  2077. expect(root.innerHTML).toBe('<!--slot-->')
  2078. })
  2079. test('vdom fallback can update to empty without crashing', async () => {
  2080. const showFallback = ref(true)
  2081. const VdomSlotWithReactiveFallback = {
  2082. render(this: any) {
  2083. return renderSlot(this.$slots, 'foo', {}, () =>
  2084. showFallback.value ? [h('div', 'fallback')] : [],
  2085. )
  2086. },
  2087. }
  2088. const VaporForwardedSlot = defineVaporComponent({
  2089. setup() {
  2090. return createComponent(
  2091. VdomSlotWithReactiveFallback,
  2092. null,
  2093. {
  2094. foo: withVaporCtx(() => createSlot('foo', null)),
  2095. },
  2096. true,
  2097. )
  2098. },
  2099. })
  2100. const root = document.createElement('div')
  2101. createVaporApp(VaporForwardedSlot).use(vaporInteropPlugin).mount(root)
  2102. expect(root.innerHTML).toBe('<div>fallback</div><!--slot-->')
  2103. showFallback.value = false
  2104. await nextTick()
  2105. expect(root.innerHTML).toBe('<!--slot-->')
  2106. showFallback.value = true
  2107. await nextTick()
  2108. expect(root.innerHTML).toBe('<div>fallback</div><!--slot-->')
  2109. })
  2110. test('vdom fallback removal clears active wrapper fallback for non-slot-fragment content', async () => {
  2111. const useFallback = ref(true)
  2112. const VdomSlotWithOptionalFallback = {
  2113. render(this: any) {
  2114. return renderSlot(
  2115. this.$slots,
  2116. 'foo',
  2117. {},
  2118. useFallback.value ? () => [h('div', 'fallback')] : undefined,
  2119. )
  2120. },
  2121. }
  2122. const VaporForwardedSlot = defineVaporComponent({
  2123. setup() {
  2124. return createComponent(
  2125. VdomSlotWithOptionalFallback,
  2126. null,
  2127. {
  2128. foo: withVaporCtx(() =>
  2129. createIf(
  2130. () => false,
  2131. () => template('<span>content</span>')(),
  2132. ),
  2133. ),
  2134. },
  2135. true,
  2136. )
  2137. },
  2138. })
  2139. const root = document.createElement('div')
  2140. createVaporApp(VaporForwardedSlot).use(vaporInteropPlugin).mount(root)
  2141. expect(root.innerHTML).toBe('<div>fallback</div><!--if-->')
  2142. useFallback.value = false
  2143. await nextTick()
  2144. expect(root.innerHTML).toBe('<!--if-->')
  2145. })
  2146. test('vdom fallback toggles between local and inherited fallback for non-slot-fragment content', async () => {
  2147. const useFallback = ref(true)
  2148. const VaporSlot = createVaporSlot('outer fallback')
  2149. const VdomForwardedSlotWithOptionalFallback = {
  2150. render(this: any) {
  2151. return h(VaporSlot, null, {
  2152. foo: () => [
  2153. renderSlot(
  2154. this.$slots,
  2155. 'foo',
  2156. {},
  2157. useFallback.value
  2158. ? () => [h('div', 'local fallback')]
  2159. : undefined,
  2160. ),
  2161. ],
  2162. _: 3 /* FORWARDED */,
  2163. })
  2164. },
  2165. }
  2166. const VaporForwardedSlot = defineVaporComponent({
  2167. setup() {
  2168. return createComponent(
  2169. VdomForwardedSlotWithOptionalFallback,
  2170. null,
  2171. {
  2172. foo: withVaporCtx(() =>
  2173. createIf(
  2174. () => false,
  2175. () => template('<span>content</span>')(),
  2176. ),
  2177. ),
  2178. },
  2179. true,
  2180. )
  2181. },
  2182. })
  2183. const root = document.createElement('div')
  2184. createVaporApp(VaporForwardedSlot).use(vaporInteropPlugin).mount(root)
  2185. expect(root.innerHTML).toBe('<div>local fallback</div><!--if-->')
  2186. useFallback.value = false
  2187. await nextTick()
  2188. expect(root.innerHTML).toBe('<div>outer fallback</div><!--if-->')
  2189. useFallback.value = true
  2190. await nextTick()
  2191. expect(root.innerHTML).toBe('<div>local fallback</div><!--if-->')
  2192. })
  2193. test('nested interop vapor slot fallback should satisfy enclosing vapor slot validity after content becomes invalid', async () => {
  2194. const showContent = ref(true)
  2195. const OuterVaporSlot = createVaporSlot('outer fallback')
  2196. const VdomSlotWithLocalFallback = {
  2197. render(this: any) {
  2198. return renderSlot(this.$slots, 'bar', {}, () => [
  2199. h('div', 'local fallback'),
  2200. ])
  2201. },
  2202. }
  2203. const NestedInteropContainer = defineVaporComponent({
  2204. setup() {
  2205. return createComponent(
  2206. VdomSlotWithLocalFallback,
  2207. null,
  2208. {
  2209. bar: withVaporCtx(() =>
  2210. createIf(
  2211. () => showContent.value,
  2212. () => template('<span>content</span>')(),
  2213. ),
  2214. ),
  2215. },
  2216. true,
  2217. )
  2218. },
  2219. })
  2220. const App = defineVaporComponent({
  2221. setup() {
  2222. return createComponent(
  2223. OuterVaporSlot,
  2224. null,
  2225. {
  2226. foo: withVaporCtx(() =>
  2227. createComponent(NestedInteropContainer, null, null),
  2228. ),
  2229. },
  2230. true,
  2231. )
  2232. },
  2233. })
  2234. const root = document.createElement('div')
  2235. createVaporApp(App).use(vaporInteropPlugin).mount(root)
  2236. await nextTick()
  2237. expect(root.innerHTML).toBe('<span>content</span><!--if--><!--slot-->')
  2238. showContent.value = false
  2239. await nextTick()
  2240. expect(root.innerHTML).toBe(
  2241. '<div>local fallback</div><!--if--><!--slot-->',
  2242. )
  2243. })
  2244. test('vdom fallback addition activates inherited vapor fallback', async () => {
  2245. const useFallback = ref(false)
  2246. const VdomSlotWithOptionalFallback = {
  2247. render(this: any) {
  2248. return renderSlot(
  2249. this.$slots,
  2250. 'foo',
  2251. {},
  2252. useFallback.value ? () => [h('div', 'fallback')] : undefined,
  2253. )
  2254. },
  2255. }
  2256. const VaporForwardedSlot = defineVaporComponent({
  2257. setup() {
  2258. return createComponent(
  2259. VdomSlotWithOptionalFallback,
  2260. null,
  2261. {
  2262. foo: withVaporCtx(() => createSlot('foo', null)),
  2263. },
  2264. true,
  2265. )
  2266. },
  2267. })
  2268. const root = document.createElement('div')
  2269. createVaporApp(VaporForwardedSlot).use(vaporInteropPlugin).mount(root)
  2270. expect(root.innerHTML).toBe('<!--slot-->')
  2271. useFallback.value = true
  2272. await nextTick()
  2273. expect(root.innerHTML).toBe('<div>fallback</div><!--slot-->')
  2274. })
  2275. test('vdom fallback addition should not remount valid forwarded vapor content', async () => {
  2276. const useFallback = ref(false)
  2277. const mountSpy = vi.fn()
  2278. const Content = defineVaporComponent({
  2279. setup() {
  2280. mountSpy()
  2281. return template('<span>content</span>')()
  2282. },
  2283. })
  2284. const VdomSlotWithOptionalFallback = {
  2285. render(this: any) {
  2286. return renderSlot(
  2287. this.$slots,
  2288. 'foo',
  2289. {},
  2290. useFallback.value ? () => [h('div', 'fallback')] : undefined,
  2291. )
  2292. },
  2293. }
  2294. const VaporForwardedSlot = defineVaporComponent({
  2295. setup() {
  2296. return createComponent(
  2297. VdomSlotWithOptionalFallback,
  2298. null,
  2299. {
  2300. foo: withVaporCtx(() => createComponent(Content, null, null)),
  2301. },
  2302. true,
  2303. )
  2304. },
  2305. })
  2306. const root = document.createElement('div')
  2307. createVaporApp(VaporForwardedSlot).use(vaporInteropPlugin).mount(root)
  2308. expect(root.innerHTML).toBe('<span>content</span>')
  2309. expect(mountSpy).toHaveBeenCalledTimes(1)
  2310. useFallback.value = true
  2311. await nextTick()
  2312. expect(root.innerHTML).toBe('<span>content</span>')
  2313. expect(mountSpy).toHaveBeenCalledTimes(1)
  2314. })
  2315. test('vdom fallback added over valid forwarded vapor content should activate later when content becomes invalid', async () => {
  2316. const useFallback = ref(false)
  2317. const showContent = ref(true)
  2318. const mountSpy = vi.fn()
  2319. const Content = defineVaporComponent({
  2320. setup() {
  2321. mountSpy()
  2322. return createIf(
  2323. () => showContent.value,
  2324. () => template('<span>content</span>')(),
  2325. )
  2326. },
  2327. })
  2328. const VdomSlotWithOptionalFallback = {
  2329. render(this: any) {
  2330. return renderSlot(
  2331. this.$slots,
  2332. 'foo',
  2333. {},
  2334. useFallback.value ? () => [h('div', 'fallback')] : undefined,
  2335. )
  2336. },
  2337. }
  2338. const VaporForwardedSlot = defineVaporComponent({
  2339. setup() {
  2340. return createComponent(
  2341. VdomSlotWithOptionalFallback,
  2342. null,
  2343. {
  2344. foo: withVaporCtx(() => createComponent(Content, null, null)),
  2345. },
  2346. true,
  2347. )
  2348. },
  2349. })
  2350. const root = document.createElement('div')
  2351. createVaporApp(VaporForwardedSlot).use(vaporInteropPlugin).mount(root)
  2352. expect(root.innerHTML).toBe('<span>content</span><!--if-->')
  2353. expect(mountSpy).toHaveBeenCalledTimes(1)
  2354. useFallback.value = true
  2355. await nextTick()
  2356. expect(root.innerHTML).toBe('<span>content</span><!--if-->')
  2357. expect(mountSpy).toHaveBeenCalledTimes(1)
  2358. showContent.value = false
  2359. await nextTick()
  2360. expect(root.innerHTML).toBe('<div>fallback</div><!--if-->')
  2361. expect(mountSpy).toHaveBeenCalledTimes(1)
  2362. })
  2363. test('vdom fallback added over forwarded vapor slot fragments should activate when slot content later becomes invalid', async () => {
  2364. const useFallback = ref(false)
  2365. const foo = ref('content')
  2366. const showContent = ref(true)
  2367. const VdomSlotWithOptionalFallback = {
  2368. render(this: any) {
  2369. return renderSlot(
  2370. this.$slots,
  2371. 'foo',
  2372. {},
  2373. useFallback.value ? () => [h('div', 'fallback')] : undefined,
  2374. )
  2375. },
  2376. }
  2377. const VaporForwardedSlot = defineVaporComponent({
  2378. setup() {
  2379. return createComponent(
  2380. VdomSlotWithOptionalFallback,
  2381. null,
  2382. {
  2383. foo: withVaporCtx(() => createSlot('foo', null)),
  2384. },
  2385. true,
  2386. )
  2387. },
  2388. })
  2389. const App = createTestApp(VaporForwardedSlot, foo, showContent)
  2390. const root = document.createElement('div')
  2391. createApp(App).use(vaporInteropPlugin).mount(root)
  2392. expect(root.innerHTML).toBe('<span>content</span>')
  2393. useFallback.value = true
  2394. await nextTick()
  2395. expect(root.innerHTML).toBe('<span>content</span>')
  2396. showContent.value = false
  2397. await nextTick()
  2398. expect(root.innerHTML).toBe('<div>fallback</div>')
  2399. })
  2400. test('vdom fallback added later should propagate to nested slot boundaries inside still-valid content', async () => {
  2401. const useFallback = ref(false)
  2402. const showInner = ref(true)
  2403. const NestedSlotContainer = defineVaporComponent({
  2404. setup() {
  2405. return [template('<span>stable</span>')(), createSlot('bar', null)]
  2406. },
  2407. })
  2408. const VdomSlotWithOptionalFallback = {
  2409. render(this: any) {
  2410. return renderSlot(
  2411. this.$slots,
  2412. 'foo',
  2413. {},
  2414. useFallback.value
  2415. ? () => [h('div', 'outer fallback')]
  2416. : undefined,
  2417. )
  2418. },
  2419. }
  2420. const VaporForwardedSlot = defineVaporComponent({
  2421. setup() {
  2422. return createComponent(
  2423. VdomSlotWithOptionalFallback,
  2424. null,
  2425. {
  2426. foo: withVaporCtx(() =>
  2427. createComponent(
  2428. NestedSlotContainer,
  2429. null,
  2430. {
  2431. bar: withVaporCtx(() =>
  2432. createIf(
  2433. () => showInner.value,
  2434. () => template('<i>inner</i>')(),
  2435. ),
  2436. ),
  2437. },
  2438. true,
  2439. ),
  2440. ),
  2441. },
  2442. true,
  2443. )
  2444. },
  2445. })
  2446. const root = document.createElement('div')
  2447. createVaporApp(VaporForwardedSlot).use(vaporInteropPlugin).mount(root)
  2448. expect(root.innerHTML).toBe(
  2449. '<span>stable</span><i>inner</i><!--if--><!--slot-->',
  2450. )
  2451. useFallback.value = true
  2452. await nextTick()
  2453. expect(root.innerHTML).toBe(
  2454. '<span>stable</span><i>inner</i><!--if--><!--slot-->',
  2455. )
  2456. showInner.value = false
  2457. await nextTick()
  2458. expect(root.innerHTML).toBe(
  2459. '<span>stable</span><div>outer fallback</div><!--if--><!--slot-->',
  2460. )
  2461. })
  2462. test('vdom fallback toggles should wait for the next nested invalidation inside still-valid content', async () => {
  2463. const useFallback = ref(false)
  2464. const showInner = ref(false)
  2465. const NestedSlotContainer = defineVaporComponent({
  2466. setup() {
  2467. return [template('<span>stable</span>')(), createSlot('bar', null)]
  2468. },
  2469. })
  2470. const VdomSlotWithOptionalFallback = {
  2471. render(this: any) {
  2472. return renderSlot(
  2473. this.$slots,
  2474. 'foo',
  2475. {},
  2476. useFallback.value
  2477. ? () => [h('div', 'outer fallback')]
  2478. : undefined,
  2479. )
  2480. },
  2481. }
  2482. const VaporForwardedSlot = defineVaporComponent({
  2483. setup() {
  2484. return createComponent(
  2485. VdomSlotWithOptionalFallback,
  2486. null,
  2487. {
  2488. foo: withVaporCtx(() =>
  2489. createComponent(
  2490. NestedSlotContainer,
  2491. null,
  2492. {
  2493. bar: withVaporCtx(() =>
  2494. createIf(
  2495. () => showInner.value,
  2496. () => template('<i>inner</i>')(),
  2497. ),
  2498. ),
  2499. },
  2500. true,
  2501. ),
  2502. ),
  2503. },
  2504. true,
  2505. )
  2506. },
  2507. })
  2508. const root = document.createElement('div')
  2509. createVaporApp(VaporForwardedSlot).use(vaporInteropPlugin).mount(root)
  2510. expect(root.innerHTML).toBe('<span>stable</span><!--if--><!--slot-->')
  2511. useFallback.value = true
  2512. await nextTick()
  2513. expect(root.innerHTML).toBe('<span>stable</span><!--if--><!--slot-->')
  2514. showInner.value = true
  2515. await nextTick()
  2516. expect(root.innerHTML).toBe(
  2517. '<span>stable</span><i>inner</i><!--if--><!--slot-->',
  2518. )
  2519. showInner.value = false
  2520. await nextTick()
  2521. expect(root.innerHTML).toBe(
  2522. '<span>stable</span><div>outer fallback</div><!--if--><!--slot-->',
  2523. )
  2524. })
  2525. test('vdom local fallback should expose inherited fallback to nested slot boundaries', async () => {
  2526. const VaporSlot = createVaporSlot('outer fallback')
  2527. const NestedFallbackContainer = defineVaporComponent({
  2528. setup() {
  2529. return [
  2530. template('<span>local stable</span>')(),
  2531. createSlot('bar', null),
  2532. ]
  2533. },
  2534. })
  2535. const VdomForwardedSlotWithNestedFallback = {
  2536. render(this: any) {
  2537. return h(VaporSlot, null, {
  2538. foo: () => [
  2539. renderSlot(this.$slots, 'foo', {}, () => [
  2540. h(NestedFallbackContainer),
  2541. ]),
  2542. ],
  2543. _: 3 /* FORWARDED */,
  2544. })
  2545. },
  2546. }
  2547. const App = createEmptyTestApp(VdomForwardedSlotWithNestedFallback)
  2548. const root = document.createElement('div')
  2549. createApp(App).use(vaporInteropPlugin).mount(root)
  2550. expect(root.innerHTML).toBe(
  2551. '<span>local stable</span><div>outer fallback</div>',
  2552. )
  2553. })
  2554. test('vdom local fallback should expose inherited fallback to nested interop vapor slot outlets', async () => {
  2555. const VaporSlot = createVaporSlot('outer fallback')
  2556. const NestedInteropContainer = defineVaporComponent({
  2557. setup() {
  2558. return createSlot('bar', null)
  2559. },
  2560. })
  2561. const localFallback = vi.fn(() => [
  2562. h(NestedInteropContainer, null, {
  2563. baz: () => [h('span', 'unused')],
  2564. }),
  2565. ])
  2566. const VdomForwardedSlotWithNestedInteropFallback = {
  2567. render(this: any) {
  2568. return h(VaporSlot, null, {
  2569. foo: () => [renderSlot(this.$slots, 'foo', {}, localFallback)],
  2570. _: 3 /* FORWARDED */,
  2571. })
  2572. },
  2573. }
  2574. const App = createEmptyTestApp(
  2575. VdomForwardedSlotWithNestedInteropFallback,
  2576. )
  2577. const root = document.createElement('div')
  2578. createApp(App).use(vaporInteropPlugin).mount(root)
  2579. expect(localFallback).toHaveBeenCalledTimes(1)
  2580. expect(root.textContent).toBe('outer fallback')
  2581. })
  2582. test('vdom local fallback should expose inherited fallback to nested interop vapor forwarded slots', async () => {
  2583. const VaporSlot = createVaporSlot('outer fallback')
  2584. const NestedVdomSlot = {
  2585. render(this: any) {
  2586. return renderSlot(this.$slots, 'bar')
  2587. },
  2588. }
  2589. const NestedInteropForwardedSlot = defineVaporComponent({
  2590. setup() {
  2591. return createComponent(
  2592. NestedVdomSlot,
  2593. null,
  2594. {
  2595. bar: withVaporCtx(() => createSlot('bar', null)),
  2596. },
  2597. true,
  2598. )
  2599. },
  2600. })
  2601. const localFallback = vi.fn(() => [h(NestedInteropForwardedSlot)])
  2602. const VdomForwardedSlotWithNestedInteropFallback = {
  2603. render(this: any) {
  2604. return h(VaporSlot, null, {
  2605. foo: () => [renderSlot(this.$slots, 'foo', {}, localFallback)],
  2606. _: 3 /* FORWARDED */,
  2607. })
  2608. },
  2609. }
  2610. const App = createEmptyTestApp(
  2611. VdomForwardedSlotWithNestedInteropFallback,
  2612. )
  2613. const root = document.createElement('div')
  2614. createApp(App).use(vaporInteropPlugin).mount(root)
  2615. expect(localFallback).toHaveBeenCalledTimes(1)
  2616. expect(root.textContent).toBe('outer fallback')
  2617. })
  2618. test('vdom local fallback should keep nested inherited vapor fallback reactive after mount', async () => {
  2619. const fallbackText = ref('outer fallback')
  2620. const VaporSlot = defineVaporComponent({
  2621. setup() {
  2622. return createSlot('foo', null, () => {
  2623. const el = template('<div></div>')()
  2624. renderEffect(() => {
  2625. setElementText(el, fallbackText.value)
  2626. })
  2627. return el
  2628. })
  2629. },
  2630. })
  2631. const NestedInteropContainer = defineVaporComponent({
  2632. setup() {
  2633. return createSlot('bar', null)
  2634. },
  2635. })
  2636. const localFallback = vi.fn(() => [
  2637. h(NestedInteropContainer, null, {
  2638. baz: () => [h('span', 'unused')],
  2639. }),
  2640. ])
  2641. const VdomForwardedSlotWithNestedInteropFallback = {
  2642. render(this: any) {
  2643. return h(VaporSlot, null, {
  2644. foo: () => [renderSlot(this.$slots, 'foo', {}, localFallback)],
  2645. _: 3 /* FORWARDED */,
  2646. })
  2647. },
  2648. }
  2649. const App = createEmptyTestApp(
  2650. VdomForwardedSlotWithNestedInteropFallback,
  2651. )
  2652. const root = document.createElement('div')
  2653. createApp(App).use(vaporInteropPlugin).mount(root)
  2654. expect(localFallback).toHaveBeenCalledTimes(1)
  2655. expect(root.textContent).toBe('outer fallback')
  2656. fallbackText.value = 'updated outer fallback'
  2657. await nextTick()
  2658. expect(root.textContent).toBe('updated outer fallback')
  2659. })
  2660. test('vdom forwarded inherited vapor fallback should clean up old fallback effects', async () => {
  2661. const show = ref(false)
  2662. const fallbackText = ref('fallback')
  2663. const fallbackRuns = vi.fn()
  2664. const VaporSlot = defineVaporComponent({
  2665. setup() {
  2666. return createSlot('foo', null, () => {
  2667. const el = template('<div></div>')()
  2668. renderEffect(() => {
  2669. fallbackRuns()
  2670. setElementText(el, fallbackText.value)
  2671. })
  2672. return el
  2673. })
  2674. },
  2675. })
  2676. const VdomForwardedSlot = {
  2677. render(this: any) {
  2678. return h(VaporSlot, null, {
  2679. foo: () => [renderSlot(this.$slots, 'foo', {}, () => [])],
  2680. _: 3 /* FORWARDED */,
  2681. })
  2682. },
  2683. }
  2684. const App = {
  2685. setup() {
  2686. return () =>
  2687. h(
  2688. VdomForwardedSlot,
  2689. null,
  2690. createSlots({ _: 2 /* DYNAMIC */ } as any, [
  2691. show.value
  2692. ? {
  2693. name: 'foo',
  2694. fn: () => [h('span', 'content')],
  2695. key: '0',
  2696. }
  2697. : undefined,
  2698. ]),
  2699. )
  2700. },
  2701. }
  2702. const root = document.createElement('div')
  2703. createApp(App).use(vaporInteropPlugin).mount(root)
  2704. expect(root.innerHTML).toBe('<div>fallback</div>')
  2705. show.value = true
  2706. await nextTick()
  2707. expect(root.innerHTML).toBe('<span>content</span>')
  2708. show.value = false
  2709. await nextTick()
  2710. expect(root.innerHTML).toBe('<div>fallback</div>')
  2711. const runsBeforeUpdate = fallbackRuns.mock.calls.length
  2712. fallbackText.value = 'updated'
  2713. await nextTick()
  2714. expect(root.innerHTML).toBe('<div>updated</div>')
  2715. expect(fallbackRuns.mock.calls.length - runsBeforeUpdate).toBe(1)
  2716. })
  2717. test('unmounted passthrough vdom fallback should not react to inherited boundary updates', async () => {
  2718. const showInner = ref(true)
  2719. const useOuterFallback = ref(false)
  2720. const localFallbackRuns = vi.fn()
  2721. const InnerVdomSlot = {
  2722. render(this: any) {
  2723. return renderSlot(this.$slots, 'bar', {}, () => {
  2724. localFallbackRuns()
  2725. return [h('div', 'inner fallback')]
  2726. })
  2727. },
  2728. }
  2729. const OuterVdomSlot = {
  2730. render(this: any) {
  2731. return renderSlot(
  2732. this.$slots,
  2733. 'foo',
  2734. {},
  2735. useOuterFallback.value
  2736. ? () => [h('div', 'outer fallback')]
  2737. : undefined,
  2738. )
  2739. },
  2740. }
  2741. const InnerBridge = defineVaporComponent({
  2742. setup() {
  2743. return createComponent(
  2744. InnerVdomSlot,
  2745. null,
  2746. {
  2747. bar: withVaporCtx(() => createSlot('bar', null)),
  2748. },
  2749. true,
  2750. )
  2751. },
  2752. })
  2753. const OuterBridge = defineVaporComponent({
  2754. setup() {
  2755. return createComponent(
  2756. OuterVdomSlot,
  2757. null,
  2758. {
  2759. foo: withVaporCtx(() => createSlot('foo', null)),
  2760. },
  2761. true,
  2762. )
  2763. },
  2764. })
  2765. const App = {
  2766. setup() {
  2767. return () =>
  2768. h(
  2769. OuterBridge,
  2770. null,
  2771. createSlots({ _: 2 /* DYNAMIC */ } as any, [
  2772. showInner.value
  2773. ? {
  2774. name: 'foo',
  2775. fn: () => [h(InnerBridge)],
  2776. key: '0',
  2777. }
  2778. : undefined,
  2779. ]),
  2780. )
  2781. },
  2782. }
  2783. const root = document.createElement('div')
  2784. createApp(App).use(vaporInteropPlugin).mount(root)
  2785. expect(root.innerHTML).toBe('<div>inner fallback</div>')
  2786. showInner.value = false
  2787. await nextTick()
  2788. expect(root.innerHTML).toBe('')
  2789. const runsBefore = localFallbackRuns.mock.calls.length
  2790. useOuterFallback.value = true
  2791. await nextTick()
  2792. useOuterFallback.value = false
  2793. await nextTick()
  2794. expect(localFallbackRuns.mock.calls.length).toBe(runsBefore)
  2795. })
  2796. test('mounted passthrough vdom local fallback ignores unrelated inherited boundary updates', async () => {
  2797. const useOuterFallback = ref(false)
  2798. const localFallbackRuns = vi.fn()
  2799. const InnerVdomSlot = {
  2800. render(this: any) {
  2801. return renderSlot(this.$slots, 'bar', {}, () => {
  2802. localFallbackRuns()
  2803. return [h('div', 'inner fallback')]
  2804. })
  2805. },
  2806. }
  2807. const OuterVdomSlot = {
  2808. render(this: any) {
  2809. return renderSlot(
  2810. this.$slots,
  2811. 'foo',
  2812. {},
  2813. useOuterFallback.value
  2814. ? () => [h('div', 'outer fallback')]
  2815. : undefined,
  2816. )
  2817. },
  2818. }
  2819. const InnerBridge = defineVaporComponent({
  2820. setup() {
  2821. return createComponent(
  2822. InnerVdomSlot,
  2823. null,
  2824. {
  2825. bar: withVaporCtx(() => createSlot('bar', null)),
  2826. },
  2827. true,
  2828. )
  2829. },
  2830. })
  2831. const OuterBridge = defineVaporComponent({
  2832. setup() {
  2833. return createComponent(
  2834. OuterVdomSlot,
  2835. null,
  2836. {
  2837. foo: withVaporCtx(() => createSlot('foo', null)),
  2838. },
  2839. true,
  2840. )
  2841. },
  2842. })
  2843. const App = {
  2844. setup() {
  2845. return () =>
  2846. h(
  2847. OuterBridge,
  2848. null,
  2849. createSlots({ _: 2 /* DYNAMIC */ } as any, [
  2850. {
  2851. name: 'foo',
  2852. fn: () => [h(InnerBridge)],
  2853. key: '0',
  2854. },
  2855. ]),
  2856. )
  2857. },
  2858. }
  2859. const root = document.createElement('div')
  2860. createApp(App).use(vaporInteropPlugin).mount(root)
  2861. expect(root.innerHTML).toBe('<div>inner fallback</div>')
  2862. const runsBefore = localFallbackRuns.mock.calls.length
  2863. useOuterFallback.value = true
  2864. await nextTick()
  2865. expect(root.innerHTML).toBe('<div>inner fallback</div>')
  2866. expect(localFallbackRuns.mock.calls.length - runsBefore).toBe(0)
  2867. })
  2868. test('failed forwarded vapor slot remount should not keep stale fallback watchers', async () => {
  2869. const values = ref([0])
  2870. const showFallback = ref(false)
  2871. const fallbackRuns = vi.fn()
  2872. const handled = vi.fn()
  2873. const VdomChild = {
  2874. render(this: any) {
  2875. return renderSlot(
  2876. this.$slots,
  2877. 'default',
  2878. {},
  2879. showFallback.value
  2880. ? () => {
  2881. fallbackRuns()
  2882. return [h('div', 'outer fallback')]
  2883. }
  2884. : undefined,
  2885. )
  2886. },
  2887. }
  2888. const VaporParent = defineVaporComponent({
  2889. setup() {
  2890. return createComponent(
  2891. VdomChild,
  2892. null,
  2893. {
  2894. $: [
  2895. () =>
  2896. createForSlots(values.value, value => ({
  2897. name: 'default',
  2898. fn: () => {
  2899. if (value === 1) {
  2900. throw new Error('slot boom')
  2901. }
  2902. return template('<span>ok</span>')()
  2903. },
  2904. })),
  2905. ],
  2906. },
  2907. true,
  2908. )
  2909. },
  2910. })
  2911. const root = document.createElement('div')
  2912. const app = createApp({
  2913. render: () => h(VaporParent as any),
  2914. })
  2915. app.use(vaporInteropPlugin)
  2916. app.config.errorHandler = handled
  2917. app.mount(root)
  2918. expect(root.innerHTML).toBe('<span>ok</span>')
  2919. values.value = [1]
  2920. await nextTick()
  2921. expect(handled).toHaveBeenCalledTimes(1)
  2922. const runsBefore = fallbackRuns.mock.calls.length
  2923. showFallback.value = true
  2924. await nextTick()
  2925. expect(fallbackRuns.mock.calls.length).toBe(runsBefore)
  2926. })
  2927. test('switching vdom fallback identity disposes stale fallback effects', async () => {
  2928. const fallbackA = ref('fallback A')
  2929. const fallbackB = ref('fallback B')
  2930. const useFallbackA = ref(true)
  2931. const VaporSlot = defineVaporComponent({
  2932. setup() {
  2933. return createSlot('foo', null) as any
  2934. },
  2935. })
  2936. const VdomForwardedSlotWithDynamicFallback = {
  2937. render(this: any) {
  2938. return h(VaporSlot as any, null, {
  2939. foo: () => [
  2940. renderSlot(
  2941. this.$slots,
  2942. 'foo',
  2943. {},
  2944. useFallbackA.value
  2945. ? () => [h('div', fallbackA.value)]
  2946. : () => [h('div', fallbackB.value)],
  2947. ),
  2948. ],
  2949. _: 3 /* FORWARDED */,
  2950. })
  2951. },
  2952. }
  2953. const VaporForwardedSlot = createVaporForwardedSlot(
  2954. VdomForwardedSlotWithDynamicFallback,
  2955. )
  2956. const App = createEmptyTestApp(VaporForwardedSlot)
  2957. const root = document.createElement('div')
  2958. createApp(App).use(vaporInteropPlugin).mount(root)
  2959. expect(root.innerHTML).toBe('<div>fallback A</div>')
  2960. fallbackA.value = 'fallback A updated'
  2961. await nextTick()
  2962. expect(root.innerHTML).toBe('<div>fallback A updated</div>')
  2963. useFallbackA.value = false
  2964. await nextTick()
  2965. expect(root.innerHTML).toBe('<div>fallback B</div>')
  2966. fallbackA.value = 'stale fallback A'
  2967. await nextTick()
  2968. expect(root.innerHTML).toBe('<div>fallback B</div>')
  2969. fallbackB.value = 'fallback B updated'
  2970. await nextTick()
  2971. expect(root.innerHTML).toBe('<div>fallback B updated</div>')
  2972. })
  2973. test('vdom slot does not evaluate fallback while forwarded vapor content resolves valid output', () => {
  2974. const fallback = vi.fn(() => [h('div', 'fallback')])
  2975. const VdomSlot = {
  2976. render(this: any) {
  2977. return renderSlot(this.$slots, 'foo', {}, fallback)
  2978. },
  2979. }
  2980. const VaporForwardedSlot = defineVaporComponent({
  2981. setup() {
  2982. return createComponent(
  2983. VdomSlot,
  2984. null,
  2985. {
  2986. foo: withVaporCtx(() =>
  2987. createIf(
  2988. () => true,
  2989. () => template('<span>content</span>')(),
  2990. ),
  2991. ),
  2992. },
  2993. true,
  2994. )
  2995. },
  2996. })
  2997. const App = createEmptyTestApp(VaporForwardedSlot)
  2998. const root = document.createElement('div')
  2999. createApp(App).use(vaporInteropPlugin).mount(root)
  3000. expect(root.textContent).toBe('content')
  3001. expect(fallback).not.toHaveBeenCalled()
  3002. })
  3003. test('vdom slot > vapor forwarded slot > vdom forwarded slot > vdom slot', async () => {
  3004. const foo = ref('foo')
  3005. const show = ref(true)
  3006. const VdomSlot = createVdomSlot()
  3007. const VdomForwardedSlot = createVdomForwardedSlot(VdomSlot)
  3008. const VaporForwardedSlot = createVaporForwardedSlot(VdomForwardedSlot)
  3009. const App = createTestApp(VaporForwardedSlot, foo, show)
  3010. const root = document.createElement('div')
  3011. createApp(App).use(vaporInteropPlugin).mount(root)
  3012. expect(root.innerHTML).toBe('<span>foo</span>')
  3013. foo.value = 'bar'
  3014. await nextTick()
  3015. expect(root.innerHTML).toBe('<span>bar</span>')
  3016. show.value = false
  3017. await nextTick()
  3018. expect(root.innerHTML).toBe('<div>fallback</div>')
  3019. show.value = true
  3020. await nextTick()
  3021. expect(root.innerHTML).toBe('<span>bar</span>')
  3022. })
  3023. test('vdom slot > vapor forwarded slot(with fallback) > vdom forwarded slot > vdom slot', async () => {
  3024. const foo = ref('foo')
  3025. const show = ref(true)
  3026. const VdomSlot = createVdomSlot()
  3027. const VdomForwardedSlot = createVdomForwardedSlot(VdomSlot)
  3028. const VaporForwardedSlotWithFallback = createVaporForwardedSlot(
  3029. VdomForwardedSlot,
  3030. 'vapor fallback',
  3031. )
  3032. const App = createTestApp(VaporForwardedSlotWithFallback, foo, show)
  3033. const root = document.createElement('div')
  3034. createApp(App).use(vaporInteropPlugin).mount(root)
  3035. expect(root.innerHTML).toBe('<span>foo</span>')
  3036. foo.value = 'bar'
  3037. await nextTick()
  3038. expect(root.innerHTML).toBe('<span>bar</span>')
  3039. show.value = false
  3040. await nextTick()
  3041. expect(root.innerHTML).toBe('<div>vapor fallback</div>')
  3042. show.value = true
  3043. await nextTick()
  3044. expect(root.innerHTML).toBe('<span>bar</span>')
  3045. })
  3046. test('vdom slot > vapor forwarded slot > vdom forwarded slot(with fallback) > vdom slot', async () => {
  3047. const foo = ref('foo')
  3048. const show = ref(true)
  3049. const VdomSlot = createVdomSlot()
  3050. const VdomForwardedSlotWithFallback = createVdomForwardedSlot(
  3051. VdomSlot,
  3052. 'vdom fallback',
  3053. )
  3054. const VaporForwardedSlot = createVaporForwardedSlot(
  3055. VdomForwardedSlotWithFallback,
  3056. )
  3057. const App = createTestApp(VaporForwardedSlot, foo, show)
  3058. const root = document.createElement('div')
  3059. createApp(App).use(vaporInteropPlugin).mount(root)
  3060. expect(root.innerHTML).toBe('<span>foo</span>')
  3061. foo.value = 'bar'
  3062. await nextTick()
  3063. expect(root.innerHTML).toBe('<span>bar</span>')
  3064. show.value = false
  3065. await nextTick()
  3066. expect(root.innerHTML).toBe('<div>vdom fallback</div>')
  3067. show.value = true
  3068. await nextTick()
  3069. expect(root.innerHTML).toBe('<span>bar</span>')
  3070. })
  3071. test('vdom slot > vapor forwarded slot (multiple) > vdom forwarded slot > vdom slot', async () => {
  3072. const foo = ref('foo')
  3073. const show = ref(true)
  3074. const VdomSlot = createVdomSlot()
  3075. const VdomForwardedSlot = createVdomForwardedSlot(VdomSlot)
  3076. const VaporForwardedSlot = createMultipleVaporForwardedSlots(
  3077. VdomForwardedSlot,
  3078. 3,
  3079. )
  3080. const App = createTestApp(VaporForwardedSlot, foo, show)
  3081. const root = document.createElement('div')
  3082. createApp(App).use(vaporInteropPlugin).mount(root)
  3083. expect(root.innerHTML).toBe('<span>foo</span><!--slot--><!--slot-->')
  3084. foo.value = 'bar'
  3085. await nextTick()
  3086. expect(root.innerHTML).toBe('<span>bar</span><!--slot--><!--slot-->')
  3087. show.value = false
  3088. await nextTick()
  3089. expect(root.innerHTML).toBe('<div>fallback</div><!--slot--><!--slot-->')
  3090. show.value = true
  3091. await nextTick()
  3092. expect(root.innerHTML).toBe('<span>bar</span><!--slot--><!--slot-->')
  3093. })
  3094. test('vdom slot > vapor forwarded slot (multiple) > vdom forwarded slot(with fallback) > vdom slot', async () => {
  3095. const foo = ref('foo')
  3096. const show = ref(true)
  3097. const VdomSlot = createVdomSlot()
  3098. const VdomForwardedSlotWithFallback = createVdomForwardedSlot(
  3099. VdomSlot,
  3100. 'vdom fallback',
  3101. )
  3102. const VaporForwardedSlot = createMultipleVaporForwardedSlots(
  3103. VdomForwardedSlotWithFallback,
  3104. 3,
  3105. )
  3106. const App = createTestApp(VaporForwardedSlot, foo, show)
  3107. const root = document.createElement('div')
  3108. createApp(App).use(vaporInteropPlugin).mount(root)
  3109. expect(root.innerHTML).toBe('<span>foo</span><!--slot--><!--slot-->')
  3110. foo.value = 'bar'
  3111. await nextTick()
  3112. expect(root.innerHTML).toBe('<span>bar</span><!--slot--><!--slot-->')
  3113. show.value = false
  3114. await nextTick()
  3115. expect(root.innerHTML).toBe(
  3116. '<div>vdom fallback</div><!--slot--><!--slot-->',
  3117. )
  3118. show.value = true
  3119. await nextTick()
  3120. expect(root.innerHTML).toBe('<span>bar</span><!--slot--><!--slot-->')
  3121. })
  3122. test('vdom slot > vdom forwarded slot > vapor slot', async () => {
  3123. const foo = ref('foo')
  3124. const show = ref(true)
  3125. const VaporSlot = createVaporSlot()
  3126. const VdomForwardedSlot = createVdomForwardedSlot(VaporSlot)
  3127. const App = createTestApp(VdomForwardedSlot, foo, show)
  3128. const root = document.createElement('div')
  3129. createApp(App).use(vaporInteropPlugin).mount(root)
  3130. expect(root.innerHTML).toBe('<span>foo</span>')
  3131. foo.value = 'bar'
  3132. await nextTick()
  3133. expect(root.innerHTML).toBe('<span>bar</span>')
  3134. show.value = false
  3135. await nextTick()
  3136. expect(root.innerHTML).toBe('<div>fallback</div>')
  3137. show.value = true
  3138. await nextTick()
  3139. expect(root.innerHTML).toBe('<span>bar</span>')
  3140. })
  3141. test('vdom slot > vdom forwarded slot > vapor forwarded slot > vapor slot', async () => {
  3142. const foo = ref('foo')
  3143. const show = ref(true)
  3144. const VaporSlot = createVaporSlot()
  3145. const VaporForwardedSlot = createVaporForwardedSlot(VaporSlot)
  3146. const VdomForwardedSlot = createVdomForwardedSlot(VaporForwardedSlot)
  3147. const App = createTestApp(VdomForwardedSlot, foo, show)
  3148. const root = document.createElement('div')
  3149. createApp(App).use(vaporInteropPlugin).mount(root)
  3150. expect(root.innerHTML).toBe('<span>foo</span><!--slot-->')
  3151. foo.value = 'bar'
  3152. await nextTick()
  3153. expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
  3154. show.value = false
  3155. await nextTick()
  3156. expect(root.innerHTML).toBe('<div>fallback</div><!--slot-->')
  3157. show.value = true
  3158. await nextTick()
  3159. expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
  3160. })
  3161. test('vdom slot > vdom forwarded slot (multiple) > vapor forwarded slot > vdom slot', async () => {
  3162. const foo = ref('foo')
  3163. const show = ref(true)
  3164. const VaporSlot = createVaporSlot()
  3165. const VaporForwardedSlot = createVaporForwardedSlot(VaporSlot)
  3166. const VdomForwardedSlot = createMultipleVdomForwardedSlots(
  3167. VaporForwardedSlot,
  3168. 3,
  3169. )
  3170. const App = createTestApp(VdomForwardedSlot, foo, show)
  3171. const root = document.createElement('div')
  3172. createApp(App).use(vaporInteropPlugin).mount(root)
  3173. expect(root.innerHTML).toBe('<span>foo</span><!--slot-->')
  3174. foo.value = 'bar'
  3175. await nextTick()
  3176. expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
  3177. show.value = false
  3178. await nextTick()
  3179. expect(root.innerHTML).toBe('<div>fallback</div><!--slot-->')
  3180. show.value = true
  3181. await nextTick()
  3182. expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
  3183. })
  3184. test('vdom slot > vdom forwarded slot (multiple) > vapor forwarded slot(with fallback) > vdom slot', async () => {
  3185. const foo = ref('foo')
  3186. const show = ref(true)
  3187. const VaporSlot = createVaporSlot()
  3188. const VaporForwardedSlot = createVaporForwardedSlot(
  3189. VaporSlot,
  3190. 'vapor fallback',
  3191. )
  3192. const VdomForwardedSlot = createMultipleVdomForwardedSlots(
  3193. VaporForwardedSlot,
  3194. 3,
  3195. )
  3196. const App = createTestApp(VdomForwardedSlot, foo, show)
  3197. const root = document.createElement('div')
  3198. createApp(App).use(vaporInteropPlugin).mount(root)
  3199. expect(root.innerHTML).toBe('<span>foo</span><!--slot-->')
  3200. foo.value = 'bar'
  3201. await nextTick()
  3202. expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
  3203. show.value = false
  3204. await nextTick()
  3205. expect(root.innerHTML).toBe('<div>vapor fallback</div><!--slot-->')
  3206. show.value = true
  3207. await nextTick()
  3208. expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
  3209. })
  3210. test('vdom slot > vapor forwarded slot > vapor forwarded slot > vdom slot', async () => {
  3211. const foo = ref('foo')
  3212. const show = ref(true)
  3213. const VdomSlot = createVdomSlot()
  3214. const VaporForwardedSlot1 = createMultipleVaporForwardedSlots(
  3215. VdomSlot,
  3216. 2,
  3217. )
  3218. const App = createTestApp(VaporForwardedSlot1, foo, show)
  3219. const root = document.createElement('div')
  3220. createApp(App).use(vaporInteropPlugin).mount(root)
  3221. expect(root.innerHTML).toBe('<span>foo</span><!--slot-->')
  3222. foo.value = 'bar'
  3223. await nextTick()
  3224. expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
  3225. show.value = false
  3226. await nextTick()
  3227. expect(root.innerHTML).toBe('<div>fallback</div><!--slot-->')
  3228. show.value = true
  3229. await nextTick()
  3230. expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
  3231. })
  3232. test('vdom slot > vapor forwarded slot(with fallback) > vapor forwarded slot > vdom slot', async () => {
  3233. const foo = ref('foo')
  3234. const show = ref(true)
  3235. const VdomSlot = createVdomSlot()
  3236. const VaporForwardedSlot2 = createVaporForwardedSlot(VdomSlot)
  3237. const VaporForwardedSlot1WithFallback = createVaporForwardedSlot(
  3238. VaporForwardedSlot2,
  3239. 'vapor1 fallback',
  3240. )
  3241. const App = createTestApp(VaporForwardedSlot1WithFallback, foo, show)
  3242. const root = document.createElement('div')
  3243. createApp(App).use(vaporInteropPlugin).mount(root)
  3244. expect(root.innerHTML).toBe('<span>foo</span><!--slot-->')
  3245. foo.value = 'bar'
  3246. await nextTick()
  3247. expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
  3248. show.value = false
  3249. await nextTick()
  3250. expect(root.innerHTML).toBe('<div>vapor1 fallback</div><!--slot-->')
  3251. show.value = true
  3252. await nextTick()
  3253. expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
  3254. })
  3255. test('vdom slot > vapor forwarded slot > vapor forwarded slot(with fallback) > vdom slot', async () => {
  3256. const foo = ref('foo')
  3257. const show = ref(true)
  3258. const VdomSlot = createVdomSlot()
  3259. const VaporForwardedSlot2WithFallback = createVaporForwardedSlot(
  3260. VdomSlot,
  3261. 'vapor2 fallback',
  3262. )
  3263. const VaporForwardedSlot1 = createVaporForwardedSlot(
  3264. VaporForwardedSlot2WithFallback,
  3265. )
  3266. const App = createTestApp(VaporForwardedSlot1, foo, show)
  3267. const root = document.createElement('div')
  3268. createApp(App).use(vaporInteropPlugin).mount(root)
  3269. expect(root.innerHTML).toBe('<span>foo</span><!--slot-->')
  3270. foo.value = 'bar'
  3271. await nextTick()
  3272. expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
  3273. show.value = false
  3274. await nextTick()
  3275. expect(root.innerHTML).toBe('<div>vapor2 fallback</div><!--slot-->')
  3276. show.value = true
  3277. await nextTick()
  3278. expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
  3279. })
  3280. test('vdom slot > vapor forwarded slot > vapor forwarded slot > vapor slot', async () => {
  3281. const foo = ref('foo')
  3282. const show = ref(true)
  3283. const VaporSlot = createVaporSlot()
  3284. const VaporForwardedSlot2 = createVaporForwardedSlot(VaporSlot)
  3285. const VaporForwardedSlot1 =
  3286. createVaporForwardedSlot(VaporForwardedSlot2)
  3287. const App = createTestApp(VaporForwardedSlot1, foo, show)
  3288. const root = document.createElement('div')
  3289. createApp(App).use(vaporInteropPlugin).mount(root)
  3290. expect(root.innerHTML).toBe('<span>foo</span><!--slot--><!--slot-->')
  3291. foo.value = 'bar'
  3292. await nextTick()
  3293. expect(root.innerHTML).toBe('<span>bar</span><!--slot--><!--slot-->')
  3294. show.value = false
  3295. await nextTick()
  3296. expect(root.innerHTML).toBe('<div>fallback</div><!--slot--><!--slot-->')
  3297. show.value = true
  3298. await nextTick()
  3299. expect(root.innerHTML).toBe('<span>bar</span><!--slot--><!--slot-->')
  3300. })
  3301. test('vdom slot > vapor forwarded slot(with fallback) > vapor forwarded slot(with fallback) > vdom slot', async () => {
  3302. const foo = ref('foo')
  3303. const show = ref(true)
  3304. const VdomSlot = createVdomSlot()
  3305. const VaporForwardedSlot2WithFallback = createVaporForwardedSlot(
  3306. VdomSlot,
  3307. 'vapor2 fallback',
  3308. )
  3309. const VaporForwardedSlot1WithFallback = createVaporForwardedSlot(
  3310. VaporForwardedSlot2WithFallback,
  3311. 'vapor1 fallback',
  3312. )
  3313. const App = createTestApp(VaporForwardedSlot1WithFallback, foo, show)
  3314. const root = document.createElement('div')
  3315. createApp(App).use(vaporInteropPlugin).mount(root)
  3316. expect(root.innerHTML).toBe('<span>foo</span><!--slot-->')
  3317. foo.value = 'bar'
  3318. await nextTick()
  3319. expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
  3320. show.value = false
  3321. await nextTick()
  3322. expect(root.innerHTML).toBe('<div>vapor1 fallback</div><!--slot-->')
  3323. show.value = true
  3324. await nextTick()
  3325. expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
  3326. })
  3327. test('vdom slot > vapor forwarded slot(with fallback) > vapor forwarded slot(with fallback) > vapor slot', async () => {
  3328. const foo = ref('foo')
  3329. const show = ref(true)
  3330. const VaporSlot = createVaporSlot()
  3331. const VaporForwardedSlot2WithFallback = createVaporForwardedSlot(
  3332. VaporSlot,
  3333. 'vapor2 fallback',
  3334. )
  3335. const VaporForwardedSlot1WithFallback = createVaporForwardedSlot(
  3336. VaporForwardedSlot2WithFallback,
  3337. 'vapor1 fallback',
  3338. )
  3339. const App = createTestApp(VaporForwardedSlot1WithFallback, foo, show)
  3340. const root = document.createElement('div')
  3341. createApp(App).use(vaporInteropPlugin).mount(root)
  3342. expect(root.innerHTML).toBe('<span>foo</span><!--slot--><!--slot-->')
  3343. foo.value = 'bar'
  3344. await nextTick()
  3345. expect(root.innerHTML).toBe('<span>bar</span><!--slot--><!--slot-->')
  3346. show.value = false
  3347. await nextTick()
  3348. expect(root.innerHTML).toBe(
  3349. '<div>vapor1 fallback</div><!--slot--><!--slot-->',
  3350. )
  3351. show.value = true
  3352. await nextTick()
  3353. expect(root.innerHTML).toBe('<span>bar</span><!--slot--><!--slot-->')
  3354. })
  3355. test('vdom slot > vdom forwarded slot(with fallback) > vdom forwarded slot(with fallback) > vapor slot', async () => {
  3356. const foo = ref('foo')
  3357. const show = ref(true)
  3358. const VaporSlot = createVaporSlot()
  3359. const VdomForwardedSlot2WithFallback = createVdomForwardedSlot(
  3360. VaporSlot,
  3361. 'vdom2 fallback',
  3362. )
  3363. const VdomForwardedSlot1WithFallback = createVdomForwardedSlot(
  3364. VdomForwardedSlot2WithFallback,
  3365. 'vdom1 fallback',
  3366. )
  3367. const App = createTestApp(VdomForwardedSlot1WithFallback, foo, show)
  3368. const root = document.createElement('div')
  3369. createApp(App).use(vaporInteropPlugin).mount(root)
  3370. expect(root.innerHTML).toBe('<span>foo</span>')
  3371. foo.value = 'bar'
  3372. await nextTick()
  3373. expect(root.innerHTML).toBe('<span>bar</span>')
  3374. show.value = false
  3375. await nextTick()
  3376. expect(root.innerHTML).toBe('<div>vdom1 fallback</div>')
  3377. show.value = true
  3378. await nextTick()
  3379. expect(root.innerHTML).toBe('<span>bar</span>')
  3380. })
  3381. test('vdom slot > vdom forwarded slot(with fallback) > vdom forwarded slot(with fallback) > vdom slot', async () => {
  3382. const foo = ref('foo')
  3383. const show = ref(true)
  3384. const VdomSlot = createVdomSlot()
  3385. const VdomForwardedSlot2WithFallback = createVdomForwardedSlot(
  3386. VdomSlot,
  3387. 'vdom2 fallback',
  3388. )
  3389. const VdomForwardedSlot1WithFallback = createVdomForwardedSlot(
  3390. VdomForwardedSlot2WithFallback,
  3391. 'vdom1 fallback',
  3392. )
  3393. const App = createTestApp(VdomForwardedSlot1WithFallback, foo, show)
  3394. const root = document.createElement('div')
  3395. createApp(App).use(vaporInteropPlugin).mount(root)
  3396. expect(root.innerHTML).toBe('<span>foo</span>')
  3397. foo.value = 'bar'
  3398. await nextTick()
  3399. expect(root.innerHTML).toBe('<span>bar</span>')
  3400. show.value = false
  3401. await nextTick()
  3402. expect(root.innerHTML).toBe('<div>vdom1 fallback</div>')
  3403. show.value = true
  3404. await nextTick()
  3405. expect(root.innerHTML).toBe('<span>bar</span>')
  3406. })
  3407. test('vdom slot > vdom forwarded slot(with fallback) > vdom forwarded slot(with fallback) (multiple) > vapor slot', async () => {
  3408. const foo = ref('foo')
  3409. const show = ref(true)
  3410. const VaporSlot = createVaporSlot()
  3411. const VdomForwardedSlot3WithFallback = createVdomForwardedSlot(
  3412. VaporSlot,
  3413. 'vdom3 fallback',
  3414. )
  3415. const VdomForwardedSlot2WithFallback = createVdomForwardedSlot(
  3416. VdomForwardedSlot3WithFallback,
  3417. 'vdom2 fallback',
  3418. )
  3419. const VdomForwardedSlot1WithFallback = createVdomForwardedSlot(
  3420. VdomForwardedSlot2WithFallback,
  3421. 'vdom1 fallback',
  3422. )
  3423. const App = createTestApp(VdomForwardedSlot1WithFallback, foo, show)
  3424. const root = document.createElement('div')
  3425. createApp(App).use(vaporInteropPlugin).mount(root)
  3426. expect(root.innerHTML).toBe('<span>foo</span>')
  3427. foo.value = 'bar'
  3428. await nextTick()
  3429. expect(root.innerHTML).toBe('<span>bar</span>')
  3430. show.value = false
  3431. await nextTick()
  3432. expect(root.innerHTML).toBe('<div>vdom1 fallback</div>')
  3433. show.value = true
  3434. await nextTick()
  3435. expect(root.innerHTML).toBe('<span>bar</span>')
  3436. })
  3437. })
  3438. })
  3439. describe('createForSlots', () => {
  3440. test('should work', async () => {
  3441. const loop = ref([1, 2, 3])
  3442. let instance: any
  3443. const Child = () => {
  3444. instance = currentInstance
  3445. return template('child')()
  3446. }
  3447. const { render } = define({
  3448. setup() {
  3449. return createComponent(Child, null, {
  3450. $: [
  3451. () =>
  3452. createForSlots(loop.value, (item, i) => ({
  3453. name: item,
  3454. fn: () => template(item + i)(),
  3455. })),
  3456. ],
  3457. })
  3458. },
  3459. })
  3460. render()
  3461. expect(instance.slots).toHaveProperty('1')
  3462. expect(instance.slots).toHaveProperty('2')
  3463. expect(instance.slots).toHaveProperty('3')
  3464. loop.value.push(4)
  3465. await nextTick()
  3466. expect(instance.slots).toHaveProperty('4')
  3467. loop.value.shift()
  3468. await nextTick()
  3469. expect(instance.slots).not.toHaveProperty('1')
  3470. })
  3471. test('should cache dynamic slot source result', async () => {
  3472. const items = ref([1, 2, 3])
  3473. let callCount = 0
  3474. const getItems = () => {
  3475. callCount++
  3476. return items.value
  3477. }
  3478. let instance: any
  3479. const Child = defineVaporComponent(() => {
  3480. instance = currentInstance
  3481. // Create multiple slots to trigger multiple getSlot calls
  3482. const n1 = template('<div></div>')()
  3483. const n2 = template('<div></div>')()
  3484. const n3 = template('<div></div>')()
  3485. insert(createSlot('slot1'), n1 as any as ParentNode)
  3486. insert(createSlot('slot2'), n2 as any as ParentNode)
  3487. insert(createSlot('slot3'), n3 as any as ParentNode)
  3488. return [n1, n2, n3]
  3489. })
  3490. define({
  3491. setup() {
  3492. return createComponent(Child, null, {
  3493. $: [
  3494. () =>
  3495. createForSlots(getItems(), (item, i) => ({
  3496. name: 'slot' + item,
  3497. fn: () => template(String(item))(),
  3498. })),
  3499. ],
  3500. })
  3501. },
  3502. }).render()
  3503. // getItems should only be called once
  3504. expect(callCount).toBe(1)
  3505. expect(instance.slots).toHaveProperty('slot1')
  3506. expect(instance.slots).toHaveProperty('slot2')
  3507. expect(instance.slots).toHaveProperty('slot3')
  3508. })
  3509. test('should update when source changes', async () => {
  3510. const items = ref([1, 2])
  3511. let callCount = 0
  3512. const getItems = () => {
  3513. callCount++
  3514. return items.value
  3515. }
  3516. let instance: any
  3517. const Child = defineVaporComponent(() => {
  3518. instance = currentInstance
  3519. const n1 = template('<div></div>')()
  3520. const n2 = template('<div></div>')()
  3521. const n3 = template('<div></div>')()
  3522. insert(createSlot('slot1'), n1 as any as ParentNode)
  3523. insert(createSlot('slot2'), n2 as any as ParentNode)
  3524. insert(createSlot('slot3'), n3 as any as ParentNode)
  3525. return [n1, n2, n3]
  3526. })
  3527. define({
  3528. setup() {
  3529. return createComponent(Child, null, {
  3530. $: [
  3531. () =>
  3532. createForSlots(getItems(), (item, i) => ({
  3533. name: 'slot' + item,
  3534. fn: () => template(String(item))(),
  3535. })),
  3536. ],
  3537. })
  3538. },
  3539. }).render()
  3540. expect(callCount).toBe(1)
  3541. expect(instance.slots).toHaveProperty('slot1')
  3542. expect(instance.slots).toHaveProperty('slot2')
  3543. expect(instance.slots).not.toHaveProperty('slot3')
  3544. // Update items
  3545. items.value.push(3)
  3546. await nextTick()
  3547. // Should be called again after source changes
  3548. expect(callCount).toBe(2)
  3549. expect(instance.slots).toHaveProperty('slot1')
  3550. expect(instance.slots).toHaveProperty('slot2')
  3551. expect(instance.slots).toHaveProperty('slot3')
  3552. })
  3553. test('should render slots correctly with caching', async () => {
  3554. const items = ref([1, 2, 3, 4, 5])
  3555. const Child = defineVaporComponent(() => {
  3556. const containers: any[] = []
  3557. for (let i = 1; i <= 5; i++) {
  3558. const n = template('<div></div>')()
  3559. insert(createSlot('slot' + i), n as any as ParentNode)
  3560. containers.push(n)
  3561. }
  3562. return containers
  3563. })
  3564. const { host } = define({
  3565. setup() {
  3566. return createComponent(Child, null, {
  3567. $: [
  3568. () =>
  3569. createForSlots(items.value, item => ({
  3570. name: 'slot' + item,
  3571. fn: () => template('content' + item)(),
  3572. })),
  3573. ],
  3574. })
  3575. },
  3576. }).render()
  3577. expect(host.innerHTML).toBe(
  3578. '<div>content1<!--slot--></div>' +
  3579. '<div>content2<!--slot--></div>' +
  3580. '<div>content3<!--slot--></div>' +
  3581. '<div>content4<!--slot--></div>' +
  3582. '<div>content5<!--slot--></div>',
  3583. )
  3584. // Update items
  3585. items.value = [2, 4]
  3586. await nextTick()
  3587. expect(host.innerHTML).toBe(
  3588. '<div><!--slot--></div>' +
  3589. '<div>content2<!--slot--></div>' +
  3590. '<div><!--slot--></div>' +
  3591. '<div>content4<!--slot--></div>' +
  3592. '<div><!--slot--></div>',
  3593. )
  3594. })
  3595. // #14648
  3596. test('should use last slot when v-for generates duplicate slot names', async () => {
  3597. const list = ref([0, 1, 2])
  3598. const Child = defineVaporComponent(() => {
  3599. const n = template('<div></div>')()
  3600. insert(createSlot('default'), n as any as ParentNode)
  3601. return n
  3602. })
  3603. const { host } = define({
  3604. setup() {
  3605. return createComponent(Child, null, {
  3606. $: [
  3607. () =>
  3608. createForSlots(list.value, item => ({
  3609. name: 'default',
  3610. fn: () => template(String(item))(),
  3611. })),
  3612. ],
  3613. })
  3614. },
  3615. }).render()
  3616. // should display the last item (last wins, matching vDOM behavior)
  3617. expect(host.innerHTML).toBe('<div>2<!--slot--></div>')
  3618. // push: new last item should be displayed
  3619. list.value.push(3)
  3620. await nextTick()
  3621. expect(host.innerHTML).toBe('<div>3<!--slot--></div>')
  3622. // pop: should fall back to previous last item
  3623. list.value.pop()
  3624. await nextTick()
  3625. expect(host.innerHTML).toBe('<div>2<!--slot--></div>')
  3626. // splice middle: last item unchanged
  3627. list.value.splice(1, 1)
  3628. await nextTick()
  3629. expect(host.innerHTML).toBe('<div>2<!--slot--></div>')
  3630. })
  3631. test('should work with null and undefined', async () => {
  3632. const loop = ref<number[] | null | undefined>(undefined)
  3633. let instance: any
  3634. const Child = () => {
  3635. instance = currentInstance
  3636. return template('child')()
  3637. }
  3638. const { render } = define({
  3639. setup() {
  3640. return createComponent(Child, null, {
  3641. $: [
  3642. () =>
  3643. createForSlots(loop.value as any, (item, i) => ({
  3644. name: item,
  3645. fn: () => template(item + i)(),
  3646. })),
  3647. ],
  3648. })
  3649. },
  3650. })
  3651. render()
  3652. expect(instance.slots).toEqual({})
  3653. loop.value = [1]
  3654. await nextTick()
  3655. expect(instance.slots).toHaveProperty('1')
  3656. loop.value = null
  3657. await nextTick()
  3658. expect(instance.slots).toEqual({})
  3659. })
  3660. })
  3661. })