componentSlots.spec.ts 120 KB

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