vdomInterop.spec.ts 93 KB

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