vdomInterop.spec.ts 87 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211
  1. import {
  2. KeepAlive,
  3. type ShallowRef,
  4. Suspense,
  5. Teleport,
  6. createApp,
  7. createCommentVNode,
  8. createVNode,
  9. defineComponent,
  10. h,
  11. inject,
  12. nextTick,
  13. onActivated,
  14. onBeforeMount,
  15. onDeactivated,
  16. onMounted,
  17. onUnmounted,
  18. provide,
  19. ref,
  20. renderSlot,
  21. resolveDynamicComponent,
  22. shallowRef,
  23. toDisplayString,
  24. useModel,
  25. useTemplateRef,
  26. vShow,
  27. withDirectives,
  28. } from '@vue/runtime-dom'
  29. import { makeInteropRender } from './_utils'
  30. import {
  31. VaporKeepAlive,
  32. VaporTeleport,
  33. applyTextModel,
  34. applyVShow,
  35. child,
  36. createComponent,
  37. createDynamicComponent,
  38. createForSlots,
  39. createIf,
  40. createSlot,
  41. createTemplateRefSetter,
  42. defineVaporAsyncComponent,
  43. defineVaporComponent,
  44. insert,
  45. renderEffect,
  46. setText,
  47. template,
  48. txt,
  49. vaporInteropPlugin,
  50. withVaporCtx,
  51. } from '../src'
  52. const define = makeInteropRender()
  53. describe('vdomInterop', () => {
  54. describe('key', () => {
  55. test('preserves vnode key on blocks passed from vdom to vapor', () => {
  56. const VDomChild = defineComponent({
  57. setup() {
  58. return () => h('div', 'vdom child')
  59. },
  60. })
  61. const app = createApp({ render: () => null })
  62. app.use(vaporInteropPlugin)
  63. const vapor = (app._context as any).vapor
  64. const vnodeBlock = vapor.vdomMountVNode(
  65. h(VDomChild, { key: 'foo' }),
  66. null,
  67. )
  68. expect(vnodeBlock.$key).toBe('foo')
  69. expect(vnodeBlock.vnode.key).toBe('foo')
  70. const componentBlock = vapor.vdomMount(VDomChild, null, { key: 'bar' })
  71. expect(componentBlock.$key).toBe('bar')
  72. expect(componentBlock.vnode.key).toBe('bar')
  73. })
  74. test('preserves single slot vnode key on interop fragments', async () => {
  75. const key = ref('foo')
  76. const CompA = defineComponent({
  77. setup() {
  78. return () => h('div', 'A')
  79. },
  80. })
  81. const CompB = defineComponent({
  82. setup() {
  83. return () => h('div', 'B')
  84. },
  85. })
  86. const current = shallowRef<any>(CompA)
  87. const VaporChild = defineVaporComponent({
  88. setup() {
  89. return createSlot('default') as any
  90. },
  91. })
  92. const Parent = defineComponent({
  93. setup() {
  94. return () =>
  95. h(VaporChild as any, null, {
  96. default: () => [h(current.value, { key: key.value })],
  97. })
  98. },
  99. })
  100. const app = createApp(Parent)
  101. app.use(vaporInteropPlugin)
  102. const vapor = (app._context as any).vapor
  103. const originalVdomSlot = vapor.vdomSlot
  104. let frag: any
  105. vapor.vdomSlot = (...args: any[]) => (frag = originalVdomSlot(...args))
  106. const host = document.createElement('div')
  107. app.mount(host)
  108. expect(frag.$key).toBe('_defaultfoo')
  109. key.value = 'bar'
  110. current.value = CompB
  111. await nextTick()
  112. expect(frag.$key).toBe('_defaultbar')
  113. })
  114. })
  115. describe('fragment nodes', () => {
  116. test('refreshes interop fragment nodes after component root updates', async () => {
  117. const show = ref(false)
  118. const VDomChild = defineComponent({
  119. setup() {
  120. return () =>
  121. show.value ? h('div', 'child') : createCommentVNode('v-if', true)
  122. },
  123. })
  124. const app = createApp({ render: () => null })
  125. app.use(vaporInteropPlugin)
  126. const vapor = (app._context as any).vapor
  127. const host = document.createElement('div')
  128. const frag = vapor.vdomMount(VDomChild, null)
  129. insert(frag, host)
  130. expect(host.innerHTML).toBe('<!--v-if-->')
  131. expect(frag.nodes).toBeInstanceOf(Comment)
  132. show.value = true
  133. await nextTick()
  134. expect(host.innerHTML).toBe('<div>child</div>')
  135. expect(frag.nodes).toBeInstanceOf(HTMLDivElement)
  136. })
  137. test('refreshes vdom slot fragment nodes after child root updates', async () => {
  138. const show = ref(false)
  139. const VDomChild = defineComponent({
  140. setup() {
  141. return () =>
  142. show.value ? h('div', 'child') : createCommentVNode('v-if', true)
  143. },
  144. })
  145. const VaporChild = defineVaporComponent({
  146. setup() {
  147. return createSlot('default') as any
  148. },
  149. })
  150. const Parent = defineComponent({
  151. setup() {
  152. return () =>
  153. h(VaporChild as any, null, {
  154. default: () => [h(VDomChild)],
  155. })
  156. },
  157. })
  158. const app = createApp(Parent)
  159. app.use(vaporInteropPlugin)
  160. const vapor = (app._context as any).vapor
  161. const originalVdomSlot = vapor.vdomSlot
  162. let frag: any
  163. vapor.vdomSlot = (...args: any[]) => (frag = originalVdomSlot(...args))
  164. const host = document.createElement('div')
  165. app.mount(host)
  166. const onUpdated = vi.fn()
  167. frag.onUpdated = [onUpdated]
  168. const getNodes = () =>
  169. (Array.isArray(frag.nodes) ? frag.nodes : [frag.nodes]).filter(Boolean)
  170. expect(host.innerHTML).toBe('<!--v-if-->')
  171. expect(getNodes().some((n: Node) => n instanceof HTMLDivElement)).toBe(
  172. false,
  173. )
  174. show.value = true
  175. await nextTick()
  176. expect(host.innerHTML).toContain('<div>child</div>')
  177. expect(getNodes().some((n: Node) => n instanceof HTMLDivElement)).toBe(
  178. true,
  179. )
  180. expect(onUpdated).toHaveBeenCalled()
  181. })
  182. })
  183. describe('props', () => {
  184. test('should work if props are not provided', () => {
  185. const VaporChild = defineVaporComponent({
  186. props: {
  187. msg: String,
  188. },
  189. setup(_, { attrs }) {
  190. return [document.createTextNode(attrs.class || 'foo')]
  191. },
  192. })
  193. const { html } = define({
  194. setup() {
  195. return () => h(VaporChild as any)
  196. },
  197. }).render()
  198. expect(html()).toBe('foo')
  199. })
  200. test('should handle class prop when vapor renders vdom component', () => {
  201. const VDomChild = defineComponent({
  202. setup() {
  203. return () => h('div', { class: 'foo' })
  204. },
  205. })
  206. const VaporChild = defineVaporComponent({
  207. setup() {
  208. return createComponent(VDomChild as any, { class: () => 'bar' })
  209. },
  210. })
  211. const { html } = define({
  212. setup() {
  213. return () => h(VaporChild as any)
  214. },
  215. }).render()
  216. expect(html()).toBe('<div class="foo bar"></div>')
  217. })
  218. test('should not pass reserved props into vapor attrs on update', async () => {
  219. const msg = ref('foo')
  220. const onVnodeMounted = vi.fn()
  221. const VaporChild = defineVaporComponent({
  222. setup(_, { attrs }) {
  223. const n0 = template(' ')() as any
  224. renderEffect(() => {
  225. setText(
  226. n0,
  227. `${String(attrs.msg)}|${String('onVnodeMounted' in attrs)}`,
  228. )
  229. })
  230. return n0
  231. },
  232. })
  233. const { html } = define({
  234. setup() {
  235. return () =>
  236. h(VaporChild as any, {
  237. msg: msg.value,
  238. onVnodeMounted,
  239. })
  240. },
  241. }).render()
  242. expect(html()).toBe('foo|false')
  243. msg.value = 'bar'
  244. await nextTick()
  245. expect(html()).toBe('bar|false')
  246. })
  247. test('should invoke onVnodeMounted and onVnodeUnmounted', async () => {
  248. const VaporChild = defineVaporComponent({
  249. setup() {
  250. return template('<div>vapor</div>')()
  251. },
  252. })
  253. const show = ref(true)
  254. const vnodeMounted = vi.fn()
  255. const vnodeUnmounted = vi.fn()
  256. const { html } = define({
  257. setup() {
  258. return () =>
  259. show.value
  260. ? h(VaporChild as any, {
  261. onVnodeMounted: vnodeMounted,
  262. onVnodeUnmounted: vnodeUnmounted,
  263. })
  264. : null
  265. },
  266. }).render()
  267. await nextTick()
  268. expect(html()).toBe('<div>vapor</div>')
  269. expect(vnodeMounted).toHaveBeenCalledTimes(1)
  270. expect(vnodeUnmounted).toHaveBeenCalledTimes(0)
  271. show.value = false
  272. await nextTick()
  273. expect(vnodeUnmounted).toHaveBeenCalledTimes(1)
  274. })
  275. test('should invoke vnode and directive mount hooks in VDOM order', async () => {
  276. const calls: string[] = []
  277. const vCustom = {
  278. created: vi.fn(() => calls.push('directive created')),
  279. beforeMount: vi.fn(() => calls.push('directive beforeMount')),
  280. mounted: vi.fn(() => calls.push('directive mounted')),
  281. }
  282. const VaporChild = defineVaporComponent({
  283. setup() {
  284. return template('<div>vapor</div>')()
  285. },
  286. })
  287. const App = defineComponent({
  288. setup() {
  289. return () =>
  290. withDirectives(
  291. h(VaporChild as any, {
  292. onVnodeBeforeMount: () => calls.push('vnode beforeMount'),
  293. onVnodeMounted: () => calls.push('vnode mounted'),
  294. }),
  295. [[vCustom]],
  296. )
  297. },
  298. })
  299. const root = document.createElement('div')
  300. const app = createApp(App)
  301. app.use(vaporInteropPlugin)
  302. app.mount(root)
  303. await nextTick()
  304. expect(calls).toEqual([
  305. 'vnode beforeMount',
  306. 'directive created',
  307. 'directive beforeMount',
  308. 'directive mounted',
  309. 'vnode mounted',
  310. ])
  311. })
  312. test('should invoke vnode and directive unmount hooks in VDOM order', async () => {
  313. const calls: string[] = []
  314. const vCustom = {
  315. beforeUnmount: vi.fn(() => calls.push('directive beforeUnmount')),
  316. unmounted: vi.fn(() => calls.push('directive unmounted')),
  317. }
  318. const VaporChild = defineVaporComponent({
  319. setup() {
  320. return template('<div>vapor</div>')()
  321. },
  322. })
  323. const show = ref(true)
  324. const { html } = define({
  325. setup() {
  326. return () =>
  327. show.value
  328. ? withDirectives(
  329. h(VaporChild as any, {
  330. onVnodeBeforeUnmount: () =>
  331. calls.push('vnode beforeUnmount'),
  332. onVnodeUnmounted: () => calls.push('vnode unmounted'),
  333. }),
  334. [[vCustom]],
  335. )
  336. : null
  337. },
  338. }).render()
  339. expect(html()).toBe('<div>vapor</div>')
  340. show.value = false
  341. await nextTick()
  342. expect(calls).toEqual([
  343. 'vnode beforeUnmount',
  344. 'directive beforeUnmount',
  345. 'directive unmounted',
  346. 'vnode unmounted',
  347. ])
  348. })
  349. test('should invoke update hooks in VDOM order on normal updates', async () => {
  350. const msg = ref('foo')
  351. const calls: string[] = []
  352. const vCustom = {
  353. beforeUpdate: vi.fn(() => calls.push('directive beforeUpdate')),
  354. updated: vi.fn(() => calls.push('directive updated')),
  355. }
  356. const VaporChild = defineVaporComponent({
  357. props: {
  358. msg: String,
  359. },
  360. setup(props: any) {
  361. const n0 = template('<div> </div>', true)() as any
  362. const x0 = child(n0) as any
  363. renderEffect(() => {
  364. setText(x0, props.msg)
  365. })
  366. return n0
  367. },
  368. })
  369. const App = defineComponent({
  370. setup() {
  371. return () =>
  372. withDirectives(
  373. h(VaporChild as any, {
  374. msg: msg.value,
  375. onVnodeBeforeUpdate: () => calls.push('vnode beforeUpdate'),
  376. onVnodeUpdated: () => calls.push('vnode updated'),
  377. }),
  378. [[vCustom]],
  379. )
  380. },
  381. })
  382. const root = document.createElement('div')
  383. const app = createApp(App)
  384. app.use(vaporInteropPlugin)
  385. app.mount(root)
  386. msg.value = 'bar'
  387. await nextTick()
  388. expect(calls).toEqual([
  389. 'vnode beforeUpdate',
  390. 'directive beforeUpdate',
  391. 'directive updated',
  392. 'vnode updated',
  393. ])
  394. })
  395. })
  396. describe('v-model', () => {
  397. test('basic work', async () => {
  398. const VaporChild = defineVaporComponent({
  399. props: {
  400. modelValue: {},
  401. modelModifiers: {},
  402. },
  403. emits: ['update:modelValue'],
  404. setup(__props) {
  405. const modelValue = useModel(__props, 'modelValue')
  406. const n0 = template('<h1> </h1>')() as any
  407. const n1 = template('<input>')() as any
  408. const x0 = child(n0) as any
  409. applyTextModel(
  410. n1,
  411. () => modelValue.value,
  412. _value => (modelValue.value = _value),
  413. )
  414. renderEffect(() => setText(x0, toDisplayString(modelValue.value)))
  415. return [n0, n1]
  416. },
  417. })
  418. const { html, host } = define({
  419. setup() {
  420. const msg = ref('foo')
  421. return () =>
  422. h(VaporChild as any, {
  423. modelValue: msg.value,
  424. 'onUpdate:modelValue': (value: string) => {
  425. msg.value = value
  426. },
  427. })
  428. },
  429. }).render()
  430. expect(html()).toBe('<h1>foo</h1><input>')
  431. const inputEl = host.querySelector('input')!
  432. inputEl.value = 'bar'
  433. inputEl.dispatchEvent(new Event('input'))
  434. await nextTick()
  435. expect(html()).toBe('<h1>bar</h1><input>')
  436. })
  437. test('slot v-model should persist when switching vapor/vdom child', async () => {
  438. const VaporComp1 = defineVaporComponent({
  439. name: 'VaporComp1',
  440. setup() {
  441. return [document.createTextNode('comp1: '), createSlot('default')]
  442. },
  443. })
  444. const VDomComp2 = defineComponent({
  445. name: 'VDomComp2',
  446. setup(_, { slots }) {
  447. return () =>
  448. h('div', [
  449. 'comp2: ',
  450. // vdom <slot/>
  451. renderSlot(slots, 'default'),
  452. ])
  453. },
  454. })
  455. const VaporParent = defineVaporComponent({
  456. name: 'VaporParent',
  457. props: {
  458. show: Boolean,
  459. modelValue: {},
  460. modelModifiers: {},
  461. },
  462. emits: ['update:modelValue'],
  463. setup(__props) {
  464. const modelValue = useModel(__props, 'modelValue')
  465. return createDynamicComponent(
  466. () => (__props.show ? VaporComp1 : VDomComp2),
  467. null,
  468. {
  469. default: () => {
  470. const input = template('<input>')() as any
  471. applyTextModel(
  472. input,
  473. () => modelValue.value,
  474. _value => (modelValue.value = _value),
  475. )
  476. return input
  477. },
  478. },
  479. true,
  480. )
  481. },
  482. })
  483. const show = ref(true)
  484. const msg = ref('')
  485. const { host } = define({
  486. setup() {
  487. return () =>
  488. h(VaporParent as any, {
  489. show: show.value,
  490. modelValue: msg.value,
  491. 'onUpdate:modelValue': (value: string) => {
  492. msg.value = value
  493. },
  494. })
  495. },
  496. }).render()
  497. const input1 = host.querySelector('input')!
  498. input1.value = 'hello'
  499. input1.dispatchEvent(new Event('input'))
  500. await nextTick()
  501. expect(msg.value).toBe('hello')
  502. show.value = false
  503. await nextTick()
  504. const input2 = host.querySelector('input')!
  505. expect(input2.value).toBe('hello')
  506. })
  507. })
  508. describe('emit', () => {
  509. test('emit from vapor child to vdom parent', () => {
  510. const VaporChild = defineVaporComponent({
  511. emits: ['click'],
  512. setup(_, { emit }) {
  513. emit('click')
  514. return []
  515. },
  516. })
  517. const fn = vi.fn()
  518. define({
  519. setup() {
  520. return () => h(VaporChild as any, { onClick: fn })
  521. },
  522. }).render()
  523. // fn should be called once
  524. expect(fn).toHaveBeenCalledTimes(1)
  525. })
  526. })
  527. describe('directives', () => {
  528. test('apply v-show to vdom child', async () => {
  529. const VDomChild = {
  530. setup() {
  531. return () => h('div')
  532. },
  533. }
  534. const show = ref(false)
  535. const VaporChild = defineVaporComponent({
  536. setup() {
  537. const n1 = createComponent(VDomChild as any)
  538. applyVShow(n1, () => show.value)
  539. return n1
  540. },
  541. })
  542. const { html } = define({
  543. setup() {
  544. return () => h(VaporChild as any)
  545. },
  546. }).render()
  547. expect(html()).toBe('<div style="display: none;"></div>')
  548. show.value = true
  549. await nextTick()
  550. expect(html()).toBe('<div style=""></div>')
  551. })
  552. test('apply v-show to vapor child', async () => {
  553. const VaporChild = defineVaporComponent({
  554. setup() {
  555. return template('<div></div>', true)()
  556. },
  557. })
  558. const show = ref(false)
  559. const App = defineComponent({
  560. setup() {
  561. return () =>
  562. h('div', null, [
  563. withDirectives(h(VaporChild as any), [[vShow, show.value]]),
  564. ])
  565. },
  566. })
  567. const root = document.createElement('div')
  568. const app = createApp(App)
  569. app.use(vaporInteropPlugin)
  570. app.mount(root)
  571. expect(root.innerHTML).toBe(
  572. '<div><div style="display: none;"></div></div>',
  573. )
  574. show.value = true
  575. await nextTick()
  576. expect(root.innerHTML).toBe('<div><div style=""></div></div>')
  577. })
  578. test('apply custom directive to vapor child', async () => {
  579. const vCustom = {
  580. created: vi.fn(),
  581. beforeMount: vi.fn(),
  582. mounted: vi.fn(),
  583. beforeUpdate: vi.fn(),
  584. updated: vi.fn(),
  585. beforeUnmount: vi.fn(),
  586. unmounted: vi.fn(),
  587. }
  588. const VaporChild = defineVaporComponent({
  589. setup() {
  590. return template('<div></div>', true)()
  591. },
  592. })
  593. const count = ref(0)
  594. const App = defineComponent({
  595. setup() {
  596. return () =>
  597. h('div', null, [
  598. withDirectives(h(VaporChild as any), [[vCustom, count.value]]),
  599. ])
  600. },
  601. })
  602. const root = document.createElement('div')
  603. const app = createApp(App)
  604. app.use(vaporInteropPlugin)
  605. app.mount(root)
  606. // root > div (App root) > div (VaporChild root)
  607. const el = root.querySelector('div')!.querySelector('div')!
  608. expect(vCustom.created).toHaveBeenCalledTimes(1)
  609. expect(vCustom.beforeMount).toHaveBeenCalledTimes(1)
  610. expect(vCustom.mounted).toHaveBeenCalledTimes(1)
  611. expect(vCustom.beforeUpdate).toHaveBeenCalledTimes(0)
  612. expect(vCustom.updated).toHaveBeenCalledTimes(0)
  613. expect(vCustom.created).toHaveBeenCalledWith(
  614. el,
  615. expect.objectContaining({ value: 0, oldValue: undefined }),
  616. expect.any(Object),
  617. null,
  618. )
  619. expect(vCustom.beforeMount).toHaveBeenCalledWith(
  620. el,
  621. expect.objectContaining({ value: 0, oldValue: undefined }),
  622. expect.any(Object),
  623. null,
  624. )
  625. expect(vCustom.mounted).toHaveBeenCalledWith(
  626. el,
  627. expect.objectContaining({ value: 0, oldValue: undefined }),
  628. expect.any(Object),
  629. null,
  630. )
  631. count.value++
  632. await nextTick()
  633. expect(vCustom.beforeUpdate).toHaveBeenCalledTimes(1)
  634. expect(vCustom.updated).toHaveBeenCalledTimes(1)
  635. expect(vCustom.beforeUpdate).toHaveBeenCalledWith(
  636. el,
  637. expect.objectContaining({ value: 1, oldValue: 0 }),
  638. expect.any(Object),
  639. expect.any(Object),
  640. )
  641. expect(vCustom.updated).toHaveBeenCalledWith(
  642. el,
  643. expect.objectContaining({ value: 1, oldValue: 0 }),
  644. expect.any(Object),
  645. expect.any(Object),
  646. )
  647. app.unmount()
  648. expect(vCustom.beforeUnmount).toHaveBeenCalledTimes(1)
  649. expect(vCustom.unmounted).toHaveBeenCalledTimes(1)
  650. expect(vCustom.beforeUnmount).toHaveBeenCalledWith(
  651. el,
  652. expect.objectContaining({ value: 1, oldValue: 0 }),
  653. expect.any(Object),
  654. null,
  655. )
  656. expect(vCustom.unmounted).toHaveBeenCalledWith(
  657. el,
  658. expect.objectContaining({ value: 1, oldValue: 0 }),
  659. expect.any(Object),
  660. null,
  661. )
  662. })
  663. test('warn on directive with non-element root vapor child', () => {
  664. const calls: string[] = []
  665. const vCustom = {
  666. created: () => calls.push('created'),
  667. beforeMount: () => calls.push('beforeMount'),
  668. mounted: () => calls.push('mounted'),
  669. beforeUpdate: () => calls.push('beforeUpdate'),
  670. updated: () => calls.push('updated'),
  671. beforeUnmount: () => calls.push('beforeUnmount'),
  672. unmounted: () => calls.push('unmounted'),
  673. }
  674. const VaporChild = defineVaporComponent({
  675. setup() {
  676. return [template('<div></div>')(), template('<div></div>')()]
  677. },
  678. })
  679. const App = defineComponent({
  680. setup() {
  681. return () =>
  682. h('div', null, [withDirectives(h(VaporChild as any), [[vCustom]])])
  683. },
  684. })
  685. const root = document.createElement('div')
  686. const app = createApp(App)
  687. app.use(vaporInteropPlugin)
  688. app.mount(root)
  689. if (__DEV__) {
  690. expect(
  691. `Runtime directive used on component with non-element root node.`,
  692. ).toHaveBeenWarned()
  693. }
  694. expect(calls.length).toBe(0)
  695. app.unmount()
  696. })
  697. test('should expose the latest vapor root element in updated hooks', async () => {
  698. const useAltRoot = ref(false)
  699. const updatedSpy = vi.fn((vnode: any) => {
  700. expect((vnode.el as Element).tagName).toBe('P')
  701. })
  702. const VaporChild = defineVaporComponent({
  703. props: {
  704. alt: Boolean,
  705. },
  706. setup(props: any) {
  707. return createIf(
  708. () => props.alt,
  709. () => template('<p>alt</p>')(),
  710. () => template('<div>base</div>')(),
  711. )
  712. },
  713. })
  714. const App = defineComponent({
  715. setup() {
  716. return () =>
  717. h(VaporChild as any, {
  718. alt: useAltRoot.value,
  719. onVnodeUpdated: updatedSpy,
  720. })
  721. },
  722. })
  723. const root = document.createElement('div')
  724. const app = createApp(App)
  725. app.use(vaporInteropPlugin)
  726. app.mount(root)
  727. useAltRoot.value = true
  728. await nextTick()
  729. expect(root.querySelector('p')).not.toBeNull()
  730. expect(updatedSpy).toHaveBeenCalledTimes(1)
  731. })
  732. })
  733. describe('slots', () => {
  734. test('basic', () => {
  735. const VDomChild = defineComponent({
  736. setup(_, { slots }) {
  737. return () => renderSlot(slots, 'default')
  738. },
  739. })
  740. const VaporChild = defineVaporComponent({
  741. setup() {
  742. return createComponent(
  743. VDomChild as any,
  744. null,
  745. {
  746. default: () => document.createTextNode('default slot'),
  747. },
  748. true,
  749. )
  750. },
  751. })
  752. const { html } = define({
  753. setup() {
  754. return () => h(VaporChild as any)
  755. },
  756. }).render()
  757. expect(html()).toBe('default slot')
  758. })
  759. test('functional slot', () => {
  760. const VDomChild = defineComponent({
  761. setup(_, { slots }) {
  762. return () => createVNode(slots.default!)
  763. },
  764. })
  765. const VaporChild = defineVaporComponent({
  766. setup() {
  767. return createComponent(
  768. VDomChild as any,
  769. null,
  770. {
  771. default: () => document.createTextNode('default slot'),
  772. },
  773. true,
  774. )
  775. },
  776. })
  777. const { html } = define({
  778. setup() {
  779. return () => h(VaporChild as any)
  780. },
  781. }).render()
  782. expect(html()).toBe('default slot')
  783. })
  784. test('slots.default() direct invocation', () => {
  785. const VDomChild = defineComponent({
  786. setup(_, { slots }) {
  787. return () => h('div', null, slots.default!())
  788. },
  789. })
  790. const VaporChild = defineVaporComponent({
  791. setup() {
  792. return createComponent(
  793. VDomChild as any,
  794. null,
  795. {
  796. default: () => template('direct call slot')(),
  797. },
  798. true,
  799. )
  800. },
  801. })
  802. const { html } = define({
  803. setup() {
  804. return () => h(VaporChild as any)
  805. },
  806. }).render()
  807. expect(html()).toBe('<div>direct call slot</div>')
  808. })
  809. test('slots.default() access should return a stable wrapper', () => {
  810. const VDomChild = defineComponent({
  811. setup(_, { slots }) {
  812. const first = slots.default
  813. const second = slots.default
  814. return () => h('div', String(first === second))
  815. },
  816. })
  817. const VaporChild = defineVaporComponent({
  818. setup() {
  819. return createComponent(
  820. VDomChild as any,
  821. null,
  822. {
  823. default: () => template('stable slot wrapper')(),
  824. },
  825. true,
  826. )
  827. },
  828. })
  829. const { html } = define({
  830. setup() {
  831. return () => h(VaporChild as any)
  832. },
  833. }).render()
  834. expect(html()).toBe('<div>true</div>')
  835. })
  836. test('slots.default() with slot props', () => {
  837. const VDomChild = defineComponent({
  838. setup(_, { slots }) {
  839. return () => h('div', null, slots.default!({ msg: 'hello' }))
  840. },
  841. })
  842. const VaporChild = defineVaporComponent({
  843. setup() {
  844. return createComponent(
  845. VDomChild as any,
  846. null,
  847. {
  848. default: (props: { msg: string }) => {
  849. const n0 = template('<span></span>')()
  850. n0.textContent = props.msg
  851. return [n0]
  852. },
  853. },
  854. true,
  855. )
  856. },
  857. })
  858. const { html } = define({
  859. setup() {
  860. return () => h(VaporChild as any)
  861. },
  862. }).render()
  863. expect(html()).toBe('<div><span>hello</span></div>')
  864. })
  865. test('slots.default() with falsy slot props should keep has/ownKeys semantics', () => {
  866. const VDomChild = defineComponent({
  867. setup(_, { slots }) {
  868. return () =>
  869. h('div', null, slots.default!({ flag: false, count: 0, text: '' }))
  870. },
  871. })
  872. const VaporChild = defineVaporComponent({
  873. setup() {
  874. return createComponent(
  875. VDomChild as any,
  876. null,
  877. {
  878. default: (props: Record<string, any>) => {
  879. const n0 = document.createTextNode(
  880. `${'flag' in props}/${'count' in props}/${'text' in props}|` +
  881. `${Object.keys(props).join(',')}|` +
  882. `${String(props.flag)},${String(props.count)},${String(props.text)}`,
  883. )
  884. return [n0]
  885. },
  886. },
  887. true,
  888. )
  889. },
  890. })
  891. const { html } = define({
  892. setup() {
  893. return () => h(VaporChild as any)
  894. },
  895. }).render()
  896. expect(html()).toBe('<div>true/true/true|flag,count,text|false,0,</div>')
  897. })
  898. test('named slot with slots[name]() invocation', () => {
  899. const VDomChild = defineComponent({
  900. setup(_, { slots }) {
  901. return () =>
  902. h('div', null, [
  903. h('header', null, slots.header!()),
  904. h('main', null, slots.default!()),
  905. h('footer', null, slots.footer!()),
  906. ])
  907. },
  908. })
  909. const VaporChild = defineVaporComponent({
  910. setup() {
  911. return createComponent(
  912. VDomChild as any,
  913. null,
  914. {
  915. header: () => template('Header')(),
  916. default: () => template('Main')(),
  917. footer: () => template('Footer')(),
  918. },
  919. true,
  920. )
  921. },
  922. })
  923. const { html } = define({
  924. setup() {
  925. return () => h(VaporChild as any)
  926. },
  927. }).render()
  928. expect(html()).toBe(
  929. '<div><header>Header</header><main>Main</main><footer>Footer</footer></div>',
  930. )
  931. })
  932. test('slots.default() return directly', () => {
  933. const VDomChild = defineComponent({
  934. setup(_, { slots }) {
  935. return () => slots.default!()
  936. },
  937. })
  938. const VaporChild = defineVaporComponent({
  939. setup() {
  940. return createComponent(
  941. VDomChild as any,
  942. null,
  943. {
  944. default: () => template('direct return slot')(),
  945. },
  946. true,
  947. )
  948. },
  949. })
  950. const { html } = define({
  951. setup() {
  952. return () => h(VaporChild as any)
  953. },
  954. }).render()
  955. expect(html()).toBe('direct return slot')
  956. })
  957. test('rendering forwarding vapor slot', () => {
  958. const VDomChild = defineComponent({
  959. setup(_, { slots }) {
  960. return () => h('div', null, { default: slots.default })
  961. },
  962. })
  963. const VaporChild = defineVaporComponent({
  964. setup() {
  965. return createComponent(
  966. VDomChild as any,
  967. null,
  968. {
  969. default: () => template('forwarded slot')(),
  970. },
  971. true,
  972. )
  973. },
  974. })
  975. const { html } = define({
  976. setup() {
  977. return () => h(VaporChild as any)
  978. },
  979. }).render()
  980. expect(html()).toBe('<div>forwarded slot</div>')
  981. })
  982. test('dynamic slots via createForSlots should update in vdom child', async () => {
  983. const list = ref([0, 1, 2])
  984. const VDomChild = defineComponent({
  985. setup(_, { slots }) {
  986. return () => h('div', null, [renderSlot(slots, 'default')])
  987. },
  988. })
  989. const VaporParent = defineVaporComponent({
  990. setup() {
  991. return createComponent(VDomChild as any, null, {
  992. $: [
  993. () =>
  994. createForSlots(list.value, value => ({
  995. name: 'default',
  996. fn: () => {
  997. const n = template('<span> </span>')() as Element
  998. const t = txt(n) as Text
  999. renderEffect(() => setText(t, toDisplayString(value)))
  1000. return n
  1001. },
  1002. })),
  1003. ],
  1004. })
  1005. },
  1006. })
  1007. const { html } = define({
  1008. setup() {
  1009. return () => h(VaporParent as any)
  1010. },
  1011. }).render()
  1012. // last-wins: shows last item
  1013. expect(html()).toBe('<div><span>2</span></div>')
  1014. list.value.push(3)
  1015. await nextTick()
  1016. expect(html()).toBe('<div><span>3</span></div>')
  1017. list.value.pop()
  1018. list.value.pop()
  1019. await nextTick()
  1020. expect(html()).toBe('<div><span>1</span></div>')
  1021. })
  1022. test('dynamic slots via createForSlots should re-mount fragment slot in vdom child', async () => {
  1023. const list = ref([0, 1, 2])
  1024. const VDomChild = defineComponent({
  1025. setup(_, { slots }) {
  1026. return () => h('div', null, [renderSlot(slots, 'default')])
  1027. },
  1028. })
  1029. const VaporParent = defineVaporComponent({
  1030. setup() {
  1031. return createComponent(VDomChild as any, null, {
  1032. $: [
  1033. () =>
  1034. createForSlots(list.value, value => ({
  1035. name: 'default',
  1036. fn: () =>
  1037. createIf(
  1038. () => true,
  1039. () => {
  1040. const n = template('<span> </span>')() as Element
  1041. const t = txt(n) as Text
  1042. renderEffect(() => setText(t, toDisplayString(value)))
  1043. return n
  1044. },
  1045. ),
  1046. })),
  1047. ],
  1048. })
  1049. },
  1050. })
  1051. const { html } = define({
  1052. setup() {
  1053. return () => h(VaporParent as any)
  1054. },
  1055. }).render()
  1056. expect(html()).toBe('<div><span>2</span><!--if--></div>')
  1057. list.value.push(3)
  1058. await nextTick()
  1059. expect(html()).toBe('<div><span>3</span><!--if--></div>')
  1060. list.value.pop()
  1061. list.value.pop()
  1062. await nextTick()
  1063. expect(html()).toBe('<div><span>1</span><!--if--></div>')
  1064. })
  1065. test('dynamic slot re-mount should stop stale effects from previous slot function', async () => {
  1066. const list = ref([0, 1])
  1067. const slotStates = new Map([
  1068. [0, { text: ref('zero'), runs: vi.fn() }],
  1069. [1, { text: ref('one'), runs: vi.fn() }],
  1070. [2, { text: ref('two'), runs: vi.fn() }],
  1071. ])
  1072. const VDomChild = defineComponent({
  1073. setup(_, { slots }) {
  1074. return () => h('div', null, [renderSlot(slots, 'default')])
  1075. },
  1076. })
  1077. const VaporParent = defineVaporComponent({
  1078. setup() {
  1079. return createComponent(VDomChild as any, null, {
  1080. $: [
  1081. () =>
  1082. createForSlots(list.value, value => ({
  1083. name: 'default',
  1084. fn: () => {
  1085. const state = slotStates.get(value)!
  1086. const n = template('<span> </span>')() as Element
  1087. const t = txt(n) as Text
  1088. renderEffect(() => {
  1089. state.runs()
  1090. setText(t, state.text.value)
  1091. })
  1092. return n
  1093. },
  1094. })),
  1095. ],
  1096. })
  1097. },
  1098. })
  1099. const { html } = define({
  1100. setup() {
  1101. return () => h(VaporParent as any)
  1102. },
  1103. }).render()
  1104. const firstState = slotStates.get(1)!
  1105. expect(html()).toBe('<div><span>one</span></div>')
  1106. expect(firstState.runs).toHaveBeenCalledTimes(1)
  1107. list.value.push(2)
  1108. await nextTick()
  1109. expect(html()).toBe('<div><span>two</span></div>')
  1110. expect(firstState.runs).toHaveBeenCalledTimes(1)
  1111. firstState.text.value = 'stale-one'
  1112. await nextTick()
  1113. expect(html()).toBe('<div><span>two</span></div>')
  1114. expect(firstState.runs).toHaveBeenCalledTimes(1)
  1115. })
  1116. test('should stop vdom slot outlet effects after outlet unmount', async () => {
  1117. const showOutlet = ref(true)
  1118. const msg = ref('one')
  1119. const track = vi.fn()
  1120. const VaporChild = defineVaporComponent({
  1121. setup() {
  1122. return createIf(
  1123. () => showOutlet.value,
  1124. () => createSlot('default'),
  1125. )
  1126. },
  1127. })
  1128. const { html } = define({
  1129. setup() {
  1130. return () =>
  1131. h(VaporChild as any, null, {
  1132. default: () => {
  1133. track()
  1134. return [h('span', msg.value)]
  1135. },
  1136. })
  1137. },
  1138. }).render()
  1139. expect(html()).toBe('<span>one</span><!--if-->')
  1140. expect(track).toHaveBeenCalledTimes(1)
  1141. showOutlet.value = false
  1142. await nextTick()
  1143. expect(html()).toBe('<!--if-->')
  1144. msg.value = 'two'
  1145. await nextTick()
  1146. expect(track).toHaveBeenCalledTimes(1)
  1147. expect(html()).toBe('<!--if-->')
  1148. })
  1149. })
  1150. describe('provide / inject', () => {
  1151. it('should inject value from vdom parent', async () => {
  1152. const VaporChild = defineVaporComponent({
  1153. setup() {
  1154. const foo = inject('foo')
  1155. const n0 = template(' ')() as any
  1156. renderEffect(() => setText(n0, toDisplayString(foo)))
  1157. return n0
  1158. },
  1159. })
  1160. const value = ref('foo')
  1161. const { html } = define({
  1162. setup() {
  1163. provide('foo', value)
  1164. return () => h(VaporChild as any)
  1165. },
  1166. }).render()
  1167. expect(html()).toBe('foo')
  1168. value.value = 'bar'
  1169. await nextTick()
  1170. expect(html()).toBe('bar')
  1171. })
  1172. })
  1173. describe('template ref', () => {
  1174. it('useTemplateRef with vapor child', async () => {
  1175. const VaporChild = defineVaporComponent({
  1176. setup(_, { expose }) {
  1177. const foo = ref('foo')
  1178. expose({ foo })
  1179. const n0 = template(' ')() as any
  1180. renderEffect(() => setText(n0, toDisplayString(foo)))
  1181. return n0
  1182. },
  1183. })
  1184. let elRef: ShallowRef
  1185. const { html } = define({
  1186. setup() {
  1187. elRef = useTemplateRef('el')
  1188. return () => h(VaporChild as any, { ref: 'el' })
  1189. },
  1190. }).render()
  1191. expect(html()).toBe('foo')
  1192. elRef!.value.foo = 'bar'
  1193. await nextTick()
  1194. expect(html()).toBe('bar')
  1195. })
  1196. it('static ref with vapor child', async () => {
  1197. const VaporChild = defineVaporComponent({
  1198. setup(_, { expose }) {
  1199. const foo = ref('foo')
  1200. expose({ foo })
  1201. const n0 = template(' ')() as any
  1202. renderEffect(() => setText(n0, toDisplayString(foo)))
  1203. return n0
  1204. },
  1205. })
  1206. let elRef: ShallowRef
  1207. const { html } = define({
  1208. setup() {
  1209. elRef = shallowRef()
  1210. return { elRef }
  1211. },
  1212. render() {
  1213. return h(VaporChild as any, { ref: 'elRef' })
  1214. },
  1215. }).render()
  1216. expect(html()).toBe('foo')
  1217. elRef!.value.foo = 'bar'
  1218. await nextTick()
  1219. expect(html()).toBe('bar')
  1220. })
  1221. it('dynamic component includes vdom component', async () => {
  1222. const vdomRef = ref<any>(null)
  1223. const VdomChild = defineComponent({
  1224. setup(_, { expose }) {
  1225. expose({ name: 'vdomChild' })
  1226. return () => h('div', 'vdom child')
  1227. },
  1228. })
  1229. const VaporChild = defineVaporComponent({
  1230. setup() {
  1231. return { vdomRef }
  1232. },
  1233. render() {
  1234. const setRef = createTemplateRefSetter()
  1235. const n0 = createDynamicComponent(() => VdomChild)
  1236. setRef(n0, vdomRef, false, 'vdomRef')
  1237. return n0
  1238. },
  1239. })
  1240. define({
  1241. setup() {
  1242. return () => h(VaporChild as any)
  1243. },
  1244. }).render()
  1245. await nextTick()
  1246. expect(vdomRef.value).toBeDefined()
  1247. expect(vdomRef.value.name).toBe('vdomChild')
  1248. })
  1249. it('dynamic component includes vdom component should cleanup old ref', async () => {
  1250. const VdomChild = defineComponent({
  1251. setup(_, { expose }) {
  1252. expose({ name: 'vdomChild' })
  1253. return () => h('div', 'vdom child')
  1254. },
  1255. })
  1256. const useA = ref(true)
  1257. const refA = ref<any>(null)
  1258. const refB = ref<any>(null)
  1259. const VaporChild = defineVaporComponent({
  1260. setup() {
  1261. const setRef = createTemplateRefSetter()
  1262. const n0 = createDynamicComponent(() => VdomChild)
  1263. renderEffect(() => {
  1264. setRef(n0, useA.value ? refA : refB, false, 'vdomRef')
  1265. })
  1266. return n0
  1267. },
  1268. })
  1269. define({
  1270. setup() {
  1271. return () => h(VaporChild as any)
  1272. },
  1273. }).render()
  1274. await nextTick()
  1275. expect(refA.value).toBeDefined()
  1276. expect(refA.value.name).toBe('vdomChild')
  1277. expect(refB.value).toBe(null)
  1278. useA.value = false
  1279. await nextTick()
  1280. expect(refA.value).toBe(null)
  1281. expect(refB.value).toBeDefined()
  1282. expect(refB.value.name).toBe('vdomChild')
  1283. })
  1284. })
  1285. describe('dynamic component', () => {
  1286. it('dynamic component with vapor child', async () => {
  1287. const VaporChild = defineVaporComponent({
  1288. setup() {
  1289. return template('<div>vapor child</div>')() as any
  1290. },
  1291. })
  1292. const VdomChild = defineComponent({
  1293. setup() {
  1294. return () => h('div', 'vdom child')
  1295. },
  1296. })
  1297. const view = shallowRef<any>(VaporChild)
  1298. const { html } = define({
  1299. setup() {
  1300. return () => h(resolveDynamicComponent(view.value) as any)
  1301. },
  1302. }).render()
  1303. expect(html()).toBe('<div>vapor child</div>')
  1304. view.value = VdomChild
  1305. await nextTick()
  1306. expect(html()).toBe('<div>vdom child</div>')
  1307. view.value = VaporChild
  1308. await nextTick()
  1309. expect(html()).toBe('<div>vapor child</div>')
  1310. })
  1311. describe('render VNodes', () => {
  1312. it('should render VNode containing vapor component from VDOM slot', async () => {
  1313. const VaporComp = defineVaporComponent({
  1314. setup() {
  1315. return template('<div>vapor comp</div>')() as any
  1316. },
  1317. })
  1318. const RouterView = defineComponent({
  1319. setup(_, { slots }) {
  1320. return () => {
  1321. const component = h(VaporComp as any)
  1322. return slots.default!({ Component: component })
  1323. }
  1324. },
  1325. })
  1326. const App = defineVaporComponent({
  1327. setup() {
  1328. return createComponent(
  1329. RouterView as any,
  1330. null,
  1331. {
  1332. default: (slotProps: { Component: any }) => {
  1333. return createDynamicComponent(() => slotProps.Component)
  1334. },
  1335. },
  1336. true,
  1337. )
  1338. },
  1339. })
  1340. const { html } = define({
  1341. setup() {
  1342. return () => h(App as any)
  1343. },
  1344. }).render()
  1345. expect(html()).toBe('<div>vapor comp</div><!--dynamic-component-->')
  1346. })
  1347. it('should render VNode containing vdom component from VDOM slot', async () => {
  1348. const VdomComp = defineComponent({
  1349. setup() {
  1350. return () => h('div', 'vdom comp')
  1351. },
  1352. })
  1353. const RouterView = defineComponent({
  1354. setup(_, { slots }) {
  1355. return () => {
  1356. const component = h(VdomComp)
  1357. return slots.default!({ Component: component })
  1358. }
  1359. },
  1360. })
  1361. const App = defineVaporComponent({
  1362. setup() {
  1363. return createComponent(
  1364. RouterView as any,
  1365. null,
  1366. {
  1367. default: (slotProps: { Component: any }) => {
  1368. return createDynamicComponent(() => slotProps.Component)
  1369. },
  1370. },
  1371. true,
  1372. )
  1373. },
  1374. })
  1375. const { html } = define({
  1376. setup() {
  1377. return () => h(App as any)
  1378. },
  1379. }).render()
  1380. expect(html()).toBe('<div>vdom comp</div><!--dynamic-component-->')
  1381. })
  1382. it('should update when VNode changes', async () => {
  1383. const VaporCompA = defineVaporComponent({
  1384. setup() {
  1385. return template('<div>vapor A</div>')() as any
  1386. },
  1387. })
  1388. const VaporCompB = defineVaporComponent({
  1389. setup() {
  1390. return template('<div>vapor B</div>')() as any
  1391. },
  1392. })
  1393. const current = shallowRef<any>(VaporCompA)
  1394. const RouterView = defineComponent({
  1395. setup(_, { slots }) {
  1396. return () => {
  1397. const component = h(current.value as any)
  1398. return slots.default!({ Component: component })
  1399. }
  1400. },
  1401. })
  1402. const App = defineVaporComponent({
  1403. setup() {
  1404. return createComponent(
  1405. RouterView as any,
  1406. null,
  1407. {
  1408. default: (slotProps: { Component: any }) => {
  1409. return createDynamicComponent(() => slotProps.Component)
  1410. },
  1411. },
  1412. true,
  1413. )
  1414. },
  1415. })
  1416. const { html } = define({
  1417. setup() {
  1418. return () => h(App as any)
  1419. },
  1420. }).render()
  1421. expect(html()).toBe('<div>vapor A</div><!--dynamic-component-->')
  1422. current.value = VaporCompB
  1423. await nextTick()
  1424. expect(html()).toBe('<div>vapor B</div><!--dynamic-component-->')
  1425. })
  1426. describe('with VaporKeepAlive', () => {
  1427. it('switch VNode with inner vapor components', async () => {
  1428. const hooksA = {
  1429. mounted: vi.fn(),
  1430. activated: vi.fn(),
  1431. deactivated: vi.fn(),
  1432. unmounted: vi.fn(),
  1433. }
  1434. const hooksB = {
  1435. mounted: vi.fn(),
  1436. activated: vi.fn(),
  1437. deactivated: vi.fn(),
  1438. unmounted: vi.fn(),
  1439. }
  1440. const VaporCompA = defineVaporComponent({
  1441. setup() {
  1442. onMounted(() => hooksA.mounted())
  1443. onActivated(() => hooksA.activated())
  1444. onDeactivated(() => hooksA.deactivated())
  1445. onUnmounted(() => hooksA.unmounted())
  1446. return template('<div>vapor A</div>')() as any
  1447. },
  1448. })
  1449. const VaporCompB = defineVaporComponent({
  1450. setup() {
  1451. onMounted(() => hooksB.mounted())
  1452. onActivated(() => hooksB.activated())
  1453. onDeactivated(() => hooksB.deactivated())
  1454. onUnmounted(() => hooksB.unmounted())
  1455. return template('<div>vapor B</div>')() as any
  1456. },
  1457. })
  1458. const current = shallowRef<any>(VaporCompA)
  1459. const RouterView = defineComponent({
  1460. setup(_, { slots }) {
  1461. return () => {
  1462. const component = h(current.value as any)
  1463. return slots.default!({ Component: component })
  1464. }
  1465. },
  1466. })
  1467. const App = defineVaporComponent({
  1468. setup() {
  1469. return createComponent(
  1470. RouterView as any,
  1471. null,
  1472. {
  1473. default: (slotProps: { Component: any }) => {
  1474. return createComponent(VaporKeepAlive, null, {
  1475. default: () =>
  1476. createDynamicComponent(() => slotProps.Component),
  1477. })
  1478. },
  1479. },
  1480. true,
  1481. )
  1482. },
  1483. })
  1484. const { html } = define({
  1485. setup() {
  1486. return () => h(App as any)
  1487. },
  1488. }).render()
  1489. expect(html()).toBe('<div>vapor A</div><!--dynamic-component-->')
  1490. // A: mounted + activated
  1491. expect(hooksA.mounted).toHaveBeenCalledTimes(1)
  1492. expect(hooksA.activated).toHaveBeenCalledTimes(1)
  1493. expect(hooksA.deactivated).toHaveBeenCalledTimes(0)
  1494. expect(hooksA.unmounted).toHaveBeenCalledTimes(0)
  1495. current.value = VaporCompB
  1496. await nextTick()
  1497. expect(html()).toBe('<div>vapor B</div><!--dynamic-component-->')
  1498. // A: deactivated (cached)
  1499. expect(hooksA.deactivated).toHaveBeenCalledTimes(1)
  1500. expect(hooksA.unmounted).toHaveBeenCalledTimes(0)
  1501. // B: mounted + activated
  1502. expect(hooksB.mounted).toHaveBeenCalledTimes(1)
  1503. expect(hooksB.activated).toHaveBeenCalledTimes(1)
  1504. current.value = VaporCompA
  1505. await nextTick()
  1506. expect(html()).toBe('<div>vapor A</div><!--dynamic-component-->')
  1507. // B: deactivated (cached)
  1508. expect(hooksB.deactivated).toHaveBeenCalledTimes(1)
  1509. expect(hooksB.unmounted).toHaveBeenCalledTimes(0)
  1510. // A: re-activated (not re-mounted)
  1511. expect(hooksA.mounted).toHaveBeenCalledTimes(1)
  1512. expect(hooksA.activated).toHaveBeenCalledTimes(2)
  1513. })
  1514. it('switch VNode with inner VDOM components', async () => {
  1515. const hooksA = {
  1516. mounted: vi.fn(),
  1517. activated: vi.fn(),
  1518. deactivated: vi.fn(),
  1519. unmounted: vi.fn(),
  1520. }
  1521. const hooksB = {
  1522. mounted: vi.fn(),
  1523. activated: vi.fn(),
  1524. deactivated: vi.fn(),
  1525. unmounted: vi.fn(),
  1526. }
  1527. const VDOMCompA = defineComponent({
  1528. setup() {
  1529. onMounted(() => hooksA.mounted())
  1530. onActivated(() => hooksA.activated())
  1531. onDeactivated(() => hooksA.deactivated())
  1532. onUnmounted(() => hooksA.unmounted())
  1533. return () => h('div', 'vdom A')
  1534. },
  1535. })
  1536. const VDOMCompB = defineComponent({
  1537. setup() {
  1538. onMounted(() => hooksB.mounted())
  1539. onActivated(() => hooksB.activated())
  1540. onDeactivated(() => hooksB.deactivated())
  1541. onUnmounted(() => hooksB.unmounted())
  1542. return () => h('div', 'vdom B')
  1543. },
  1544. })
  1545. const current = shallowRef<any>(VDOMCompA)
  1546. const RouterView = defineComponent({
  1547. setup(_, { slots }) {
  1548. return () => {
  1549. const component = h(current.value as any)
  1550. return slots.default!({ Component: component })
  1551. }
  1552. },
  1553. })
  1554. const App = defineVaporComponent({
  1555. setup() {
  1556. return createComponent(
  1557. RouterView as any,
  1558. null,
  1559. {
  1560. default: (slotProps: { Component: any }) => {
  1561. return createComponent(VaporKeepAlive, null, {
  1562. default: () =>
  1563. createDynamicComponent(() => slotProps.Component),
  1564. })
  1565. },
  1566. },
  1567. true,
  1568. )
  1569. },
  1570. })
  1571. const { html } = define({
  1572. setup() {
  1573. return () => h(App as any)
  1574. },
  1575. }).render()
  1576. expect(html()).toBe('<div>vdom A</div><!--dynamic-component-->')
  1577. // A: mounted + activated
  1578. expect(hooksA.mounted).toHaveBeenCalledTimes(1)
  1579. expect(hooksA.activated).toHaveBeenCalledTimes(1)
  1580. expect(hooksA.deactivated).toHaveBeenCalledTimes(0)
  1581. expect(hooksA.unmounted).toHaveBeenCalledTimes(0)
  1582. current.value = VDOMCompB
  1583. await nextTick()
  1584. expect(html()).toBe('<div>vdom B</div><!--dynamic-component-->')
  1585. // A: deactivated (cached)
  1586. expect(hooksA.deactivated).toHaveBeenCalledTimes(1)
  1587. expect(hooksA.unmounted).toHaveBeenCalledTimes(0)
  1588. // B: mounted + activated
  1589. expect(hooksB.mounted).toHaveBeenCalledTimes(1)
  1590. expect(hooksB.activated).toHaveBeenCalledTimes(1)
  1591. current.value = VDOMCompA
  1592. await nextTick()
  1593. expect(html()).toBe('<div>vdom A</div><!--dynamic-component-->')
  1594. // B: deactivated (cached)
  1595. expect(hooksB.deactivated).toHaveBeenCalledTimes(1)
  1596. expect(hooksB.unmounted).toHaveBeenCalledTimes(0)
  1597. // A: re-activated (not re-mounted)
  1598. expect(hooksA.mounted).toHaveBeenCalledTimes(1)
  1599. expect(hooksA.activated).toHaveBeenCalledTimes(2)
  1600. })
  1601. it('switch VNode with inner mixed vapor/VDOM components', async () => {
  1602. const hooksA = {
  1603. mounted: vi.fn(),
  1604. activated: vi.fn(),
  1605. deactivated: vi.fn(),
  1606. unmounted: vi.fn(),
  1607. }
  1608. const hooksB = {
  1609. mounted: vi.fn(),
  1610. activated: vi.fn(),
  1611. deactivated: vi.fn(),
  1612. unmounted: vi.fn(),
  1613. }
  1614. const VaporCompA = defineVaporComponent({
  1615. setup() {
  1616. onMounted(() => hooksA.mounted())
  1617. onActivated(() => hooksA.activated())
  1618. onDeactivated(() => hooksA.deactivated())
  1619. onUnmounted(() => hooksA.unmounted())
  1620. return template('<div>vapor A</div>')()
  1621. },
  1622. })
  1623. const VDOMCompB = defineComponent({
  1624. setup() {
  1625. onMounted(() => hooksB.mounted())
  1626. onActivated(() => hooksB.activated())
  1627. onDeactivated(() => hooksB.deactivated())
  1628. onUnmounted(() => hooksB.unmounted())
  1629. return () => h('div', 'vdom B')
  1630. },
  1631. })
  1632. const current = shallowRef<any>(VaporCompA)
  1633. const RouterView = defineComponent({
  1634. setup(_, { slots }) {
  1635. return () => {
  1636. const component = h(current.value as any)
  1637. return slots.default!({ Component: component })
  1638. }
  1639. },
  1640. })
  1641. const App = defineVaporComponent({
  1642. setup() {
  1643. return createComponent(
  1644. RouterView as any,
  1645. null,
  1646. {
  1647. default: (slotProps: { Component: any }) => {
  1648. return createComponent(VaporKeepAlive, null, {
  1649. default: () =>
  1650. createDynamicComponent(() => slotProps.Component),
  1651. })
  1652. },
  1653. },
  1654. true,
  1655. )
  1656. },
  1657. })
  1658. const { html } = define({
  1659. setup() {
  1660. return () => h(App as any)
  1661. },
  1662. }).render()
  1663. expect(html()).toBe('<div>vapor A</div><!--dynamic-component-->')
  1664. // A (vapor): mounted + activated
  1665. expect(hooksA.mounted).toHaveBeenCalledTimes(1)
  1666. expect(hooksA.activated).toHaveBeenCalledTimes(1)
  1667. expect(hooksA.deactivated).toHaveBeenCalledTimes(0)
  1668. expect(hooksA.unmounted).toHaveBeenCalledTimes(0)
  1669. current.value = VDOMCompB
  1670. await nextTick()
  1671. expect(html()).toBe('<div>vdom B</div><!--dynamic-component-->')
  1672. // A (vapor): deactivated (cached)
  1673. expect(hooksA.deactivated).toHaveBeenCalledTimes(1)
  1674. expect(hooksA.unmounted).toHaveBeenCalledTimes(0)
  1675. // B (vdom): mounted + activated
  1676. expect(hooksB.mounted).toHaveBeenCalledTimes(1)
  1677. expect(hooksB.activated).toHaveBeenCalledTimes(1)
  1678. current.value = VaporCompA
  1679. await nextTick()
  1680. expect(html()).toBe('<div>vapor A</div><!--dynamic-component-->')
  1681. // B (vdom): deactivated (cached)
  1682. expect(hooksB.deactivated).toHaveBeenCalledTimes(1)
  1683. expect(hooksB.unmounted).toHaveBeenCalledTimes(0)
  1684. // A (vapor): re-activated (not re-mounted)
  1685. expect(hooksA.mounted).toHaveBeenCalledTimes(1)
  1686. expect(hooksA.activated).toHaveBeenCalledTimes(2)
  1687. })
  1688. })
  1689. })
  1690. })
  1691. describe('attribute fallthrough', () => {
  1692. it('should fallthrough attrs to vdom child', () => {
  1693. const VDomChild = defineComponent({
  1694. setup() {
  1695. return () => h('div')
  1696. },
  1697. })
  1698. const VaporChild = defineVaporComponent({
  1699. setup() {
  1700. return createComponent(
  1701. VDomChild as any,
  1702. { foo: () => 'vapor foo' },
  1703. null,
  1704. true,
  1705. )
  1706. },
  1707. })
  1708. const { html } = define({
  1709. setup() {
  1710. return () => h(VaporChild as any, { foo: 'foo', bar: 'bar' })
  1711. },
  1712. }).render()
  1713. expect(html()).toBe('<div foo="foo" bar="bar"></div>')
  1714. })
  1715. it('should not fallthrough emit handlers to vdom child', () => {
  1716. const VDomChild = defineComponent({
  1717. emits: ['click'],
  1718. setup(_, { emit }) {
  1719. return () => h('button', { onClick: () => emit('click') }, 'click me')
  1720. },
  1721. })
  1722. const fn = vi.fn()
  1723. const VaporChild = defineVaporComponent({
  1724. emits: ['click'],
  1725. setup() {
  1726. return createComponent(
  1727. VDomChild as any,
  1728. { onClick: () => fn },
  1729. null,
  1730. true,
  1731. )
  1732. },
  1733. })
  1734. const { host, html } = define({
  1735. setup() {
  1736. return () => h(VaporChild as any)
  1737. },
  1738. }).render()
  1739. expect(html()).toBe('<button>click me</button>')
  1740. const button = host.querySelector('button')!
  1741. button.dispatchEvent(new Event('click'))
  1742. // fn should be called once
  1743. expect(fn).toHaveBeenCalledTimes(1)
  1744. })
  1745. it('should update attrs passed from vapor parent to vdom child', async () => {
  1746. const msg = ref('foo')
  1747. const VDomChild = defineComponent({
  1748. setup(_, { attrs }) {
  1749. return () =>
  1750. h(
  1751. 'div',
  1752. {
  1753. 'data-msg': attrs['data-msg'] as string,
  1754. },
  1755. attrs['data-msg'] as string,
  1756. )
  1757. },
  1758. })
  1759. const VaporChild = defineVaporComponent({
  1760. setup() {
  1761. return createComponent(
  1762. VDomChild as any,
  1763. {
  1764. 'data-msg': () => msg.value,
  1765. },
  1766. null,
  1767. true,
  1768. )
  1769. },
  1770. })
  1771. const { html } = define({
  1772. setup() {
  1773. return () => h(VaporChild as any)
  1774. },
  1775. }).render()
  1776. expect(html()).toBe('<div data-msg="foo">foo</div>')
  1777. msg.value = 'bar'
  1778. await nextTick()
  1779. expect(html()).toBe('<div data-msg="bar">bar</div>')
  1780. })
  1781. })
  1782. describe('async component', () => {
  1783. const duration = 5
  1784. test('render vapor async component', async () => {
  1785. const VdomChild = {
  1786. setup() {
  1787. return () => h('div', 'foo')
  1788. },
  1789. }
  1790. const VaporAsyncChild = defineVaporAsyncComponent({
  1791. loader: () => {
  1792. return new Promise(r => {
  1793. setTimeout(() => {
  1794. r(VdomChild as any)
  1795. }, duration)
  1796. })
  1797. },
  1798. loadingComponent: () => h('span', 'loading...'),
  1799. })
  1800. const { html } = define({
  1801. setup() {
  1802. return () => h(VaporAsyncChild as any)
  1803. },
  1804. }).render()
  1805. expect(html()).toBe('<span>loading...</span><!--async component-->')
  1806. await new Promise(r => setTimeout(r, duration))
  1807. await nextTick()
  1808. expect(html()).toBe('<div>foo</div><!--async component-->')
  1809. })
  1810. })
  1811. describe('KeepAlive', () => {
  1812. function assertHookCalls(
  1813. hooks: {
  1814. beforeMount: any
  1815. mounted: any
  1816. activated: any
  1817. deactivated: any
  1818. unmounted: any
  1819. },
  1820. callCounts: number[],
  1821. ) {
  1822. expect([
  1823. hooks.beforeMount.mock.calls.length,
  1824. hooks.mounted.mock.calls.length,
  1825. hooks.activated.mock.calls.length,
  1826. hooks.deactivated.mock.calls.length,
  1827. hooks.unmounted.mock.calls.length,
  1828. ]).toEqual(callCounts)
  1829. }
  1830. let hooks: any
  1831. beforeEach(() => {
  1832. hooks = {
  1833. beforeMount: vi.fn(),
  1834. mounted: vi.fn(),
  1835. activated: vi.fn(),
  1836. deactivated: vi.fn(),
  1837. unmounted: vi.fn(),
  1838. }
  1839. })
  1840. test('render vapor component', async () => {
  1841. const VaporChild = defineVaporComponent({
  1842. setup() {
  1843. const msg = ref('vapor')
  1844. onBeforeMount(() => hooks.beforeMount())
  1845. onMounted(() => hooks.mounted())
  1846. onActivated(() => hooks.activated())
  1847. onDeactivated(() => hooks.deactivated())
  1848. onUnmounted(() => hooks.unmounted())
  1849. const n0 = template('<input type="text">', true)() as any
  1850. applyTextModel(
  1851. n0,
  1852. () => msg.value,
  1853. _value => (msg.value = _value),
  1854. )
  1855. return n0
  1856. },
  1857. })
  1858. const show = ref(true)
  1859. const toggle = ref(true)
  1860. const { html, host } = define({
  1861. setup() {
  1862. return () =>
  1863. show.value
  1864. ? h(KeepAlive, null, {
  1865. default: () => (toggle.value ? h(VaporChild as any) : null),
  1866. })
  1867. : null
  1868. },
  1869. }).render()
  1870. expect(html()).toBe('<input type="text">')
  1871. let inputEl = host.firstChild as HTMLInputElement
  1872. expect(inputEl.value).toBe('vapor')
  1873. assertHookCalls(hooks, [1, 1, 1, 0, 0])
  1874. // change input value
  1875. inputEl.value = 'changed'
  1876. inputEl.dispatchEvent(new Event('input'))
  1877. await nextTick()
  1878. // deactivate
  1879. toggle.value = false
  1880. await nextTick()
  1881. expect(html()).toBe('<!---->')
  1882. assertHookCalls(hooks, [1, 1, 1, 1, 0])
  1883. // activate
  1884. toggle.value = true
  1885. await nextTick()
  1886. expect(html()).toBe('<input type="text">')
  1887. inputEl = host.firstChild as HTMLInputElement
  1888. expect(inputEl.value).toBe('changed')
  1889. assertHookCalls(hooks, [1, 1, 2, 1, 0])
  1890. // unmount keepalive
  1891. show.value = false
  1892. await nextTick()
  1893. expect(html()).toBe('<!---->')
  1894. assertHookCalls(hooks, [1, 1, 2, 2, 1])
  1895. // mount keepalive
  1896. show.value = true
  1897. await nextTick()
  1898. inputEl = host.firstChild as HTMLInputElement
  1899. expect(inputEl.value).toBe('vapor')
  1900. assertHookCalls(hooks, [2, 2, 3, 2, 1])
  1901. })
  1902. test('render vapor slot', async () => {
  1903. const show = ref(true)
  1904. const VDomComp = defineComponent({
  1905. setup(_, { slots }) {
  1906. return () => renderSlot(slots, 'default')
  1907. },
  1908. })
  1909. const App = defineVaporComponent({
  1910. setup() {
  1911. const n5 = createComponent(VaporKeepAlive, null, {
  1912. default: () =>
  1913. createIf(
  1914. () => show.value,
  1915. () =>
  1916. createComponent(VDomComp as any, null, {
  1917. default: () => template('slot text')(),
  1918. }),
  1919. ),
  1920. })
  1921. return n5
  1922. },
  1923. })
  1924. const { html } = define({
  1925. setup() {
  1926. return () => h(App)
  1927. },
  1928. }).render()
  1929. expect(html()).toBe('slot text<!--if-->')
  1930. show.value = false
  1931. await nextTick()
  1932. expect(html()).toBe('<!--if-->')
  1933. show.value = true
  1934. await nextTick()
  1935. expect(html()).toBe('slot text<!--if-->')
  1936. })
  1937. test('vdom slot fallback inside VaporKeepAlive should preserve render context', async () => {
  1938. const show = ref(true)
  1939. const VDomComp = defineComponent({
  1940. setup(_, { slots }) {
  1941. return () => renderSlot(slots, 'default')
  1942. },
  1943. })
  1944. const VaporFallback = defineVaporComponent({
  1945. setup() {
  1946. onBeforeMount(() => hooks.beforeMount())
  1947. onMounted(() => hooks.mounted())
  1948. onActivated(() => hooks.activated())
  1949. onDeactivated(() => hooks.deactivated())
  1950. onUnmounted(() => hooks.unmounted())
  1951. return template('<div>fallback</div>')() as any
  1952. },
  1953. })
  1954. const App = defineVaporComponent({
  1955. setup() {
  1956. return createComponent(VaporKeepAlive, null, {
  1957. default: withVaporCtx(() =>
  1958. createIf(
  1959. () => show.value,
  1960. () =>
  1961. createComponent(
  1962. VDomComp as any,
  1963. null,
  1964. {
  1965. default: withVaporCtx(() =>
  1966. createSlot('default', null, () =>
  1967. createComponent(VaporFallback as any),
  1968. ),
  1969. ),
  1970. },
  1971. true,
  1972. ),
  1973. ),
  1974. ),
  1975. })
  1976. },
  1977. })
  1978. const { html } = define({
  1979. setup() {
  1980. return () => h(App)
  1981. },
  1982. }).render()
  1983. expect(html()).toBe('<div>fallback</div><!--if-->')
  1984. assertHookCalls(hooks, [1, 1, 1, 0, 0])
  1985. show.value = false
  1986. await nextTick()
  1987. expect(html()).toBe('<!--if-->')
  1988. assertHookCalls(hooks, [1, 1, 1, 1, 0])
  1989. show.value = true
  1990. await nextTick()
  1991. expect(html()).toBe('<div>fallback</div><!--if-->')
  1992. assertHookCalls(hooks, [1, 1, 2, 1, 0])
  1993. })
  1994. test('unmounting vapor slot should remove vnode slot content', async () => {
  1995. const show = ref(true)
  1996. const VaporSlotOutlet = defineVaporComponent({
  1997. setup() {
  1998. return createSlot('default')
  1999. },
  2000. })
  2001. const { html } = define({
  2002. setup() {
  2003. return () =>
  2004. h('div', null, [
  2005. show.value
  2006. ? h(VaporSlotOutlet as any, null, {
  2007. default: () => [h('span', 'slot vnode')],
  2008. })
  2009. : null,
  2010. ])
  2011. },
  2012. }).render()
  2013. expect(html()).toBe('<div><span>slot vnode</span></div>')
  2014. show.value = false
  2015. await nextTick()
  2016. expect(html()).toBe('<div><!----></div>')
  2017. })
  2018. test('should update props on reactivation of vapor child in vdom KeepAlive', async () => {
  2019. const VaporChild = defineVaporComponent({
  2020. props: { msg: String },
  2021. setup(props: any) {
  2022. const n0 = template('<div> </div>')() as any
  2023. const x0 = child(n0) as any
  2024. renderEffect(() => setText(x0, props.msg))
  2025. return n0
  2026. },
  2027. })
  2028. const VdomChild = defineComponent({
  2029. setup() {
  2030. return () => h('span', 'vdom')
  2031. },
  2032. })
  2033. const current = shallowRef<any>(VaporChild)
  2034. const msg = ref('hello')
  2035. const App = defineComponent({
  2036. setup() {
  2037. return () =>
  2038. h(KeepAlive, null, {
  2039. default: () =>
  2040. h(
  2041. resolveDynamicComponent(current.value) as any,
  2042. current.value === VaporChild ? { msg: msg.value } : null,
  2043. ),
  2044. })
  2045. },
  2046. })
  2047. const root = document.createElement('div')
  2048. const app = createApp(App)
  2049. app.use(vaporInteropPlugin)
  2050. app.mount(root)
  2051. expect(root.innerHTML).toBe('<div>hello</div>')
  2052. // Switch to vdom child (deactivates vapor child)
  2053. current.value = VdomChild
  2054. await nextTick()
  2055. expect(root.innerHTML).toBe('<span>vdom</span>')
  2056. // Change props while vapor child is deactivated
  2057. msg.value = 'updated'
  2058. await nextTick()
  2059. expect(root.innerHTML).toBe('<span>vdom</span>')
  2060. // Reactivate vapor child — should reflect new props
  2061. current.value = VaporChild
  2062. await nextTick()
  2063. expect(root.innerHTML).toBe('<div>updated</div>')
  2064. })
  2065. test('should invoke vnode hooks on activate/deactivate', async () => {
  2066. const VaporChild = defineVaporComponent({
  2067. setup() {
  2068. return template('<div>vapor</div>')()
  2069. },
  2070. })
  2071. const VdomChild = defineComponent({
  2072. setup() {
  2073. return () => h('span', 'vdom')
  2074. },
  2075. })
  2076. const current = shallowRef<any>(VaporChild)
  2077. const vnodeMounted = vi.fn()
  2078. const vnodeUnmounted = vi.fn()
  2079. const App = defineComponent({
  2080. setup() {
  2081. return () =>
  2082. h(KeepAlive, null, {
  2083. default: () =>
  2084. h(
  2085. resolveDynamicComponent(current.value) as any,
  2086. current.value === VaporChild
  2087. ? {
  2088. onVnodeMounted: vnodeMounted,
  2089. onVnodeUnmounted: vnodeUnmounted,
  2090. }
  2091. : null,
  2092. ),
  2093. })
  2094. },
  2095. })
  2096. const root = document.createElement('div')
  2097. const app = createApp(App)
  2098. app.use(vaporInteropPlugin)
  2099. app.mount(root)
  2100. await nextTick()
  2101. expect(vnodeMounted).toHaveBeenCalledTimes(1)
  2102. expect(vnodeUnmounted).toHaveBeenCalledTimes(0)
  2103. // Deactivate vapor child
  2104. current.value = VdomChild
  2105. await nextTick()
  2106. expect(vnodeUnmounted).toHaveBeenCalledTimes(1)
  2107. // Reactivate vapor child
  2108. current.value = VaporChild
  2109. await nextTick()
  2110. expect(vnodeMounted).toHaveBeenCalledTimes(2)
  2111. })
  2112. test('should invoke update hooks in VDOM order on reactivation', async () => {
  2113. const VaporChild = defineVaporComponent({
  2114. props: ['msg'],
  2115. setup(props: any) {
  2116. return template('<div></div>')()
  2117. },
  2118. })
  2119. const VdomChild = defineComponent({
  2120. setup() {
  2121. return () => h('span', 'vdom')
  2122. },
  2123. })
  2124. const current = shallowRef<any>(VaporChild)
  2125. const msg = ref('hello')
  2126. const calls: string[] = []
  2127. const vDir = {
  2128. beforeUpdate: vi.fn(() => calls.push('directive beforeUpdate')),
  2129. updated: vi.fn(() => calls.push('directive updated')),
  2130. }
  2131. const App = defineComponent({
  2132. setup() {
  2133. return () =>
  2134. h(KeepAlive, null, {
  2135. default: () =>
  2136. current.value === VaporChild
  2137. ? withDirectives(
  2138. h(VaporChild as any, {
  2139. msg: msg.value,
  2140. onVnodeBeforeUpdate: () =>
  2141. calls.push('vnode beforeUpdate'),
  2142. onVnodeUpdated: () => calls.push('vnode updated'),
  2143. }),
  2144. [[vDir]],
  2145. )
  2146. : h(VdomChild),
  2147. })
  2148. },
  2149. })
  2150. const root = document.createElement('div')
  2151. const app = createApp(App)
  2152. app.use(vaporInteropPlugin)
  2153. app.mount(root)
  2154. await nextTick()
  2155. // Deactivate vapor child
  2156. current.value = VdomChild
  2157. await nextTick()
  2158. // Change props while deactivated
  2159. msg.value = 'world'
  2160. // Reactivate — should trigger update hooks
  2161. current.value = VaporChild
  2162. await nextTick()
  2163. expect(calls).toEqual([
  2164. 'vnode beforeUpdate',
  2165. 'directive beforeUpdate',
  2166. 'directive updated',
  2167. 'vnode updated',
  2168. ])
  2169. })
  2170. test('should invoke updated before activated on reactivation', async () => {
  2171. const calls: string[] = []
  2172. const VaporChild = defineVaporComponent({
  2173. props: ['msg'],
  2174. setup(props: any) {
  2175. onActivated(() => calls.push('activated'))
  2176. return template('<div></div>')()
  2177. },
  2178. })
  2179. const VdomChild = defineComponent({
  2180. setup() {
  2181. return () => h('span', 'vdom')
  2182. },
  2183. })
  2184. const current = shallowRef<any>(VaporChild)
  2185. const msg = ref('one')
  2186. const App = defineComponent({
  2187. setup() {
  2188. return () =>
  2189. h(KeepAlive, null, {
  2190. default: () =>
  2191. current.value === VaporChild
  2192. ? h(VaporChild as any, {
  2193. msg: msg.value,
  2194. onVnodeUpdated: () => calls.push('vnode updated'),
  2195. onVnodeMounted: () => calls.push('vnode mounted'),
  2196. })
  2197. : h(VdomChild),
  2198. })
  2199. },
  2200. })
  2201. const root = document.createElement('div')
  2202. const app = createApp(App)
  2203. app.use(vaporInteropPlugin)
  2204. app.mount(root)
  2205. await nextTick()
  2206. calls.length = 0
  2207. current.value = VdomChild
  2208. await nextTick()
  2209. msg.value = 'two'
  2210. current.value = VaporChild
  2211. await nextTick()
  2212. expect(calls).toEqual(['vnode updated', 'activated', 'vnode mounted'])
  2213. })
  2214. test('should expose the latest vapor root element in updated hooks on reactivation', async () => {
  2215. const VaporChild = defineVaporComponent({
  2216. props: {
  2217. alt: Boolean,
  2218. },
  2219. setup(props: any) {
  2220. return createIf(
  2221. () => props.alt,
  2222. () => template('<p>alt</p>')(),
  2223. () => template('<div>base</div>')(),
  2224. )
  2225. },
  2226. })
  2227. const VdomChild = defineComponent({
  2228. setup() {
  2229. return () => h('span', 'vdom')
  2230. },
  2231. })
  2232. const current = shallowRef<any>(VaporChild)
  2233. const useAltRoot = ref(false)
  2234. const updatedSpy = vi.fn((vnode: any) => {
  2235. expect((vnode.el as Element).tagName).toBe('P')
  2236. })
  2237. const App = defineComponent({
  2238. setup() {
  2239. return () =>
  2240. h(KeepAlive, null, {
  2241. default: () =>
  2242. current.value === VaporChild
  2243. ? h(VaporChild as any, {
  2244. alt: useAltRoot.value,
  2245. onVnodeUpdated: updatedSpy,
  2246. })
  2247. : h(VdomChild),
  2248. })
  2249. },
  2250. })
  2251. const root = document.createElement('div')
  2252. const app = createApp(App)
  2253. app.use(vaporInteropPlugin)
  2254. app.mount(root)
  2255. await nextTick()
  2256. current.value = VdomChild
  2257. await nextTick()
  2258. useAltRoot.value = true
  2259. current.value = VaporChild
  2260. await nextTick()
  2261. expect(root.querySelector('p')).not.toBeNull()
  2262. expect(updatedSpy).toHaveBeenCalledTimes(1)
  2263. })
  2264. test('should bail out directive beforeUpdate/updated on reactivation for non-element root vapor child', async () => {
  2265. const beforeUpdateSpy = vi.fn()
  2266. const updatedSpy = vi.fn()
  2267. const vDir = {
  2268. beforeUpdate: beforeUpdateSpy,
  2269. updated: updatedSpy,
  2270. }
  2271. const VaporChild = defineVaporComponent({
  2272. props: ['msg'],
  2273. setup() {
  2274. return [template('<div></div>')(), template('<div></div>')()]
  2275. },
  2276. })
  2277. const VdomChild = defineComponent({
  2278. setup() {
  2279. return () => h('span', 'vdom')
  2280. },
  2281. })
  2282. const current = shallowRef<any>(VaporChild)
  2283. const msg = ref('hello')
  2284. const App = defineComponent({
  2285. setup() {
  2286. return () =>
  2287. h(KeepAlive, null, {
  2288. default: () =>
  2289. current.value === VaporChild
  2290. ? withDirectives(h(VaporChild as any, { msg: msg.value }), [
  2291. [vDir],
  2292. ])
  2293. : h(VdomChild),
  2294. })
  2295. },
  2296. })
  2297. const root = document.createElement('div')
  2298. const app = createApp(App)
  2299. app.use(vaporInteropPlugin)
  2300. app.mount(root)
  2301. await nextTick()
  2302. if (__DEV__) {
  2303. expect(
  2304. `Runtime directive used on component with non-element root node.`,
  2305. ).toHaveBeenWarnedTimes(1)
  2306. }
  2307. expect(beforeUpdateSpy).toHaveBeenCalledTimes(0)
  2308. expect(updatedSpy).toHaveBeenCalledTimes(0)
  2309. current.value = VdomChild
  2310. await nextTick()
  2311. msg.value = 'world'
  2312. current.value = VaporChild
  2313. await nextTick()
  2314. expect(
  2315. `Runtime directive used on component with non-element root node.`,
  2316. ).toHaveBeenWarnedTimes(2)
  2317. expect(beforeUpdateSpy).toHaveBeenCalledTimes(0)
  2318. expect(updatedSpy).toHaveBeenCalledTimes(0)
  2319. })
  2320. })
  2321. describe('Teleport', () => {
  2322. test('mounts VDOM Teleport from createDynamicComponent', async () => {
  2323. const target = document.createElement('div')
  2324. target.id = 'interop-teleport-target'
  2325. document.body.appendChild(target)
  2326. try {
  2327. const VaporChild = defineVaporComponent({
  2328. setup() {
  2329. return createDynamicComponent(
  2330. () => Teleport,
  2331. { to: () => '#interop-teleport-target' },
  2332. {
  2333. default: () => template('<span>teleported</span>')(),
  2334. },
  2335. true,
  2336. )
  2337. },
  2338. })
  2339. define({
  2340. setup() {
  2341. return () => h(VaporChild as any)
  2342. },
  2343. }).render()
  2344. await nextTick()
  2345. expect(target.innerHTML).toContain('<span>teleported</span>')
  2346. } finally {
  2347. target.remove()
  2348. }
  2349. })
  2350. test('should patch vdom slot content in the new teleport target after move', async () => {
  2351. const targetA = document.createElement('div')
  2352. targetA.id = 'interop-slot-target-a'
  2353. const targetB = document.createElement('div')
  2354. targetB.id = 'interop-slot-target-b'
  2355. document.body.append(targetA, targetB)
  2356. const to = ref('#interop-slot-target-a')
  2357. const useAltRoot = ref(false)
  2358. try {
  2359. const VaporChild = defineVaporComponent({
  2360. setup() {
  2361. return createComponent(
  2362. VaporTeleport,
  2363. {
  2364. to: () => to.value,
  2365. },
  2366. {
  2367. default: () => createSlot('default'),
  2368. },
  2369. )
  2370. },
  2371. })
  2372. const App = defineComponent({
  2373. setup() {
  2374. return () =>
  2375. h(VaporChild as any, null, {
  2376. default: () => [
  2377. useAltRoot.value ? h('p', 'moved') : h('div', 'initial'),
  2378. ],
  2379. })
  2380. },
  2381. })
  2382. const host = document.createElement('div')
  2383. const app = createApp(App)
  2384. app.use(vaporInteropPlugin)
  2385. app.mount(host)
  2386. await nextTick()
  2387. expect(targetA.innerHTML).toBe('<div>initial</div>')
  2388. expect(targetB.innerHTML).toBe('')
  2389. to.value = '#interop-slot-target-b'
  2390. await nextTick()
  2391. expect(targetA.innerHTML).toBe('')
  2392. expect(targetB.innerHTML).toBe('<div>initial</div>')
  2393. useAltRoot.value = true
  2394. await nextTick()
  2395. expect(targetA.innerHTML).toBe('')
  2396. expect(targetB.innerHTML).toBe('<p>moved</p>')
  2397. } finally {
  2398. targetA.remove()
  2399. targetB.remove()
  2400. }
  2401. })
  2402. })
  2403. describe('Suspense', () => {
  2404. test('renders vapor async wrapper inside VDOM Suspense', async () => {
  2405. const duration = 5
  2406. const VaporAsyncChild = defineVaporAsyncComponent({
  2407. loader: () =>
  2408. new Promise(resolve => {
  2409. setTimeout(() => {
  2410. resolve(
  2411. defineVaporComponent({
  2412. setup() {
  2413. return template('<div><button>click</button></div>')()
  2414. },
  2415. }) as any,
  2416. )
  2417. }, duration)
  2418. }),
  2419. })
  2420. const VaporParent = defineVaporComponent({
  2421. setup() {
  2422. return createComponent(
  2423. Suspense as any,
  2424. null,
  2425. {
  2426. default: () => createComponent(VaporAsyncChild, null, null, true),
  2427. fallback: () => template('loading')(),
  2428. },
  2429. true,
  2430. )
  2431. },
  2432. })
  2433. const { html } = define({
  2434. setup() {
  2435. return () => h(VaporParent as any)
  2436. },
  2437. }).render()
  2438. expect(html()).toContain('loading')
  2439. await new Promise(resolve => setTimeout(resolve, duration + 1))
  2440. await nextTick()
  2441. expect(html()).toContain('<div><button>click</button></div>')
  2442. })
  2443. test('does not suspend vapor async wrapper with suspensible false inside VDOM Suspense', async () => {
  2444. const duration = 5
  2445. const VaporAsyncChild = defineVaporAsyncComponent({
  2446. loader: () =>
  2447. new Promise(resolve => {
  2448. setTimeout(() => {
  2449. resolve(
  2450. defineVaporComponent({
  2451. setup() {
  2452. return template('<div><button>click</button></div>')()
  2453. },
  2454. }) as any,
  2455. )
  2456. }, duration)
  2457. }),
  2458. suspensible: false,
  2459. })
  2460. const VaporParent = defineVaporComponent({
  2461. setup() {
  2462. return createComponent(
  2463. Suspense as any,
  2464. null,
  2465. {
  2466. default: () => createComponent(VaporAsyncChild, null, null, true),
  2467. fallback: () => template('loading')(),
  2468. },
  2469. true,
  2470. )
  2471. },
  2472. })
  2473. const { html } = define({
  2474. setup() {
  2475. return () => h(VaporParent as any)
  2476. },
  2477. }).render()
  2478. expect(html()).not.toContain('loading')
  2479. expect(html()).toContain('<!--async component-->')
  2480. await new Promise(resolve => setTimeout(resolve, duration + 1))
  2481. await nextTick()
  2482. expect(html()).toContain('<div><button>click</button></div>')
  2483. })
  2484. test('renders error component for vapor async wrapper inside VDOM Suspense', async () => {
  2485. const tick = () => new Promise(resolve => setTimeout(resolve))
  2486. let reject!: (error: Error) => void
  2487. const VaporAsyncChild = defineVaporAsyncComponent({
  2488. loader: () =>
  2489. new Promise((_resolve, _reject) => {
  2490. reject = _reject as (error: Error) => void
  2491. }),
  2492. errorComponent: defineVaporComponent({
  2493. props: ['error'],
  2494. setup(props: { error: Error }) {
  2495. return template(props.error.message)()
  2496. },
  2497. }),
  2498. })
  2499. const VaporParent = defineVaporComponent({
  2500. setup() {
  2501. return createComponent(
  2502. Suspense as any,
  2503. null,
  2504. {
  2505. default: () => createComponent(VaporAsyncChild, null, null, true),
  2506. fallback: () => template('loading')(),
  2507. },
  2508. true,
  2509. )
  2510. },
  2511. })
  2512. const host = document.createElement('div')
  2513. const app = createApp({
  2514. render: () => h(VaporParent as any),
  2515. })
  2516. const errorHandler = vi.fn()
  2517. app.use(vaporInteropPlugin)
  2518. app.config.errorHandler = errorHandler
  2519. try {
  2520. app.mount(host)
  2521. expect(host.innerHTML).toContain('loading')
  2522. reject(new Error('errored out'))
  2523. await tick()
  2524. await nextTick()
  2525. expect(errorHandler).toHaveBeenCalled()
  2526. expect(host.innerHTML).toContain('errored out')
  2527. } finally {
  2528. app.unmount()
  2529. host.remove()
  2530. }
  2531. })
  2532. test('does not fall through slots to error component for vapor async wrapper inside VDOM Suspense', async () => {
  2533. const tick = () => new Promise(resolve => setTimeout(resolve))
  2534. let reject!: (error: Error) => void
  2535. const VaporAsyncChild = defineVaporAsyncComponent({
  2536. loader: () =>
  2537. new Promise((_resolve, _reject) => {
  2538. reject = _reject as (error: Error) => void
  2539. }),
  2540. errorComponent: defineVaporComponent({
  2541. setup() {
  2542. const n0 = template('<div>error</div>')()
  2543. insert(createSlot('default'), n0 as any as ParentNode)
  2544. return n0
  2545. },
  2546. }),
  2547. })
  2548. const VaporParent = defineVaporComponent({
  2549. setup() {
  2550. return createComponent(
  2551. Suspense as any,
  2552. null,
  2553. {
  2554. default: () =>
  2555. createComponent(
  2556. VaporAsyncChild,
  2557. null,
  2558. {
  2559. default: withVaporCtx(() =>
  2560. template('<span>slot content</span>')(),
  2561. ),
  2562. },
  2563. true,
  2564. ),
  2565. fallback: () => template('loading')(),
  2566. },
  2567. true,
  2568. )
  2569. },
  2570. })
  2571. const host = document.createElement('div')
  2572. const app = createApp({
  2573. render: () => h(VaporParent as any),
  2574. })
  2575. app.use(vaporInteropPlugin)
  2576. const errorHandler = vi.fn()
  2577. app.config.errorHandler = errorHandler
  2578. try {
  2579. app.mount(host)
  2580. expect(host.innerHTML).toContain('loading')
  2581. reject(new Error('errored out'))
  2582. await tick()
  2583. await nextTick()
  2584. expect(errorHandler).toHaveBeenCalled()
  2585. expect(host.innerHTML).toContain('error')
  2586. expect(host.innerHTML).not.toContain('slot content')
  2587. } finally {
  2588. app.unmount()
  2589. host.remove()
  2590. }
  2591. })
  2592. test('renders async setup vapor component inside VDOM Suspense', async () => {
  2593. const duration = 5
  2594. const VaporAsyncChild = defineVaporComponent({
  2595. async setup() {
  2596. await new Promise(resolve => setTimeout(resolve, duration))
  2597. return template('<div><button>click</button></div>')()
  2598. },
  2599. })
  2600. const VaporParent = defineVaporComponent({
  2601. setup() {
  2602. return createComponent(
  2603. Suspense as any,
  2604. null,
  2605. {
  2606. default: () => createComponent(VaporAsyncChild, null, null, true),
  2607. fallback: () => template('loading')(),
  2608. },
  2609. true,
  2610. )
  2611. },
  2612. })
  2613. const { html } = define({
  2614. setup() {
  2615. return () => h(VaporParent as any)
  2616. },
  2617. }).render()
  2618. expect(html()).toContain('loading')
  2619. await new Promise(resolve => setTimeout(resolve, duration + 1))
  2620. await nextTick()
  2621. expect(html()).toContain('<div><button>click</button></div>')
  2622. })
  2623. test('renders async VDOM child inside VDOM Suspense', async () => {
  2624. const duration = 5
  2625. const VDomAsyncChild = defineComponent({
  2626. async setup() {
  2627. await new Promise(resolve => setTimeout(resolve, duration))
  2628. return () => h('div', [h('button', 'click')])
  2629. },
  2630. })
  2631. const VaporParent = defineVaporComponent({
  2632. setup() {
  2633. return createComponent(
  2634. Suspense as any,
  2635. null,
  2636. {
  2637. default: () =>
  2638. createComponent(VDomAsyncChild as any, null, null, true),
  2639. fallback: () => template('loading')(),
  2640. },
  2641. true,
  2642. )
  2643. },
  2644. })
  2645. const { html } = define({
  2646. setup() {
  2647. return () => h(VaporParent as any)
  2648. },
  2649. }).render()
  2650. expect(html()).toContain('loading')
  2651. await new Promise(resolve => setTimeout(resolve, duration + 1))
  2652. await nextTick()
  2653. expect(html()).toContain('<div><button>click</button></div>')
  2654. })
  2655. test('renders async VDOM child from vapor slot outlet inside VDOM Suspense', async () => {
  2656. const duration = 5
  2657. const VaporSlotOutlet = defineVaporComponent({
  2658. setup() {
  2659. return createSlot('default')
  2660. },
  2661. })
  2662. const VDomAsyncChild = defineComponent({
  2663. async setup() {
  2664. await new Promise(resolve => setTimeout(resolve, duration))
  2665. return () => h('div', 'slot async')
  2666. },
  2667. })
  2668. const App = defineComponent({
  2669. setup() {
  2670. return () =>
  2671. h(Suspense, null, {
  2672. default: () =>
  2673. h(VaporSlotOutlet as any, null, {
  2674. default: () => [h(VDomAsyncChild as any)],
  2675. }),
  2676. fallback: () => h('div', 'loading'),
  2677. })
  2678. },
  2679. })
  2680. const { html } = define(App).render()
  2681. expect(html()).toContain('loading')
  2682. await new Promise(resolve => setTimeout(resolve, duration + 1))
  2683. await nextTick()
  2684. expect(html()).toContain('<div>slot async</div>')
  2685. })
  2686. test('renders async VDOM vnode via createDynamicComponent inside VDOM Suspense', async () => {
  2687. const duration = 5
  2688. const VDomAsyncChild = defineComponent({
  2689. async setup() {
  2690. await new Promise(resolve => setTimeout(resolve, duration))
  2691. return () => h('button', 'vnode async')
  2692. },
  2693. })
  2694. const VaporParent = defineVaporComponent({
  2695. setup() {
  2696. return createComponent(
  2697. Suspense as any,
  2698. null,
  2699. {
  2700. default: () =>
  2701. createDynamicComponent(
  2702. () => h(VDomAsyncChild as any),
  2703. null,
  2704. null,
  2705. true,
  2706. ),
  2707. fallback: () => template('loading')(),
  2708. },
  2709. true,
  2710. )
  2711. },
  2712. })
  2713. const { html } = define({
  2714. setup() {
  2715. return () => h(VaporParent as any)
  2716. },
  2717. }).render()
  2718. expect(html()).toContain('loading')
  2719. await new Promise(resolve => setTimeout(resolve, duration + 1))
  2720. await nextTick()
  2721. expect(html()).toContain('<button>vnode async</button>')
  2722. })
  2723. test('mounts VDOM Suspense from createDynamicComponent', async () => {
  2724. const VaporChild = defineVaporComponent({
  2725. setup() {
  2726. return createDynamicComponent(
  2727. () => Suspense,
  2728. null,
  2729. {
  2730. default: () => template('<span>resolved</span>')(),
  2731. fallback: () => template('<span>fallback</span>')(),
  2732. },
  2733. true,
  2734. )
  2735. },
  2736. })
  2737. const { html } = define({
  2738. setup() {
  2739. return () => h(VaporChild as any)
  2740. },
  2741. }).render()
  2742. await nextTick()
  2743. expect(html()).toContain('<span>resolved</span>')
  2744. })
  2745. })
  2746. describe('vnode hooks', () => {
  2747. test('should invoke onVnodeBeforeMount/onVnodeBeforeUnmount on vapor child', async () => {
  2748. const beforeMountSpy = vi.fn()
  2749. const beforeUnmountSpy = vi.fn()
  2750. const VaporChild = defineVaporComponent({
  2751. setup() {
  2752. return template('<div>vapor</div>')()
  2753. },
  2754. })
  2755. const show = ref(true)
  2756. const App = defineComponent({
  2757. setup() {
  2758. return () =>
  2759. show.value
  2760. ? h(VaporChild as any, {
  2761. onVnodeBeforeMount: beforeMountSpy,
  2762. onVnodeBeforeUnmount: beforeUnmountSpy,
  2763. })
  2764. : null
  2765. },
  2766. })
  2767. const root = document.createElement('div')
  2768. const app = createApp(App)
  2769. app.use(vaporInteropPlugin)
  2770. app.mount(root)
  2771. await nextTick()
  2772. expect(beforeMountSpy).toHaveBeenCalledTimes(1)
  2773. // unmount
  2774. show.value = false
  2775. await nextTick()
  2776. expect(beforeUnmountSpy).toHaveBeenCalledTimes(1)
  2777. })
  2778. })
  2779. })