componentSlots.spec.ts 123 KB

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