vdomInterop.spec.ts 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869
  1. import {
  2. KeepAlive,
  3. type ShallowRef,
  4. Suspense,
  5. Teleport,
  6. createApp,
  7. createVNode,
  8. defineComponent,
  9. h,
  10. inject,
  11. nextTick,
  12. onActivated,
  13. onBeforeMount,
  14. onDeactivated,
  15. onMounted,
  16. onUnmounted,
  17. provide,
  18. ref,
  19. renderSlot,
  20. resolveDynamicComponent,
  21. shallowRef,
  22. toDisplayString,
  23. useModel,
  24. useTemplateRef,
  25. vShow,
  26. withDirectives,
  27. } from '@vue/runtime-dom'
  28. import { makeInteropRender } from './_utils'
  29. import {
  30. VaporKeepAlive,
  31. applyTextModel,
  32. applyVShow,
  33. child,
  34. createComponent,
  35. createDynamicComponent,
  36. createIf,
  37. createSlot,
  38. createTemplateRefSetter,
  39. defineVaporAsyncComponent,
  40. defineVaporComponent,
  41. renderEffect,
  42. setText,
  43. template,
  44. vaporInteropPlugin,
  45. } from '../src'
  46. const define = makeInteropRender()
  47. describe('vdomInterop', () => {
  48. describe('props', () => {
  49. test('should work if props are not provided', () => {
  50. const VaporChild = defineVaporComponent({
  51. props: {
  52. msg: String,
  53. },
  54. setup(_, { attrs }) {
  55. return [document.createTextNode(attrs.class || 'foo')]
  56. },
  57. })
  58. const { html } = define({
  59. setup() {
  60. return () => h(VaporChild as any)
  61. },
  62. }).render()
  63. expect(html()).toBe('foo')
  64. })
  65. test('should handle class prop when vapor renders vdom component', () => {
  66. const VDomChild = defineComponent({
  67. setup() {
  68. return () => h('div', { class: 'foo' })
  69. },
  70. })
  71. const VaporChild = defineVaporComponent({
  72. setup() {
  73. return createComponent(VDomChild as any, { class: () => 'bar' })
  74. },
  75. })
  76. const { html } = define({
  77. setup() {
  78. return () => h(VaporChild as any)
  79. },
  80. }).render()
  81. expect(html()).toBe('<div class="foo bar"></div>')
  82. })
  83. test('should not pass reserved props into vapor attrs on update', async () => {
  84. const msg = ref('foo')
  85. const onVnodeMounted = vi.fn()
  86. const VaporChild = defineVaporComponent({
  87. setup(_, { attrs }) {
  88. const n0 = template(' ')() as any
  89. renderEffect(() => {
  90. setText(
  91. n0,
  92. `${String(attrs.msg)}|${String('onVnodeMounted' in attrs)}`,
  93. )
  94. })
  95. return n0
  96. },
  97. })
  98. const { html } = define({
  99. setup() {
  100. return () =>
  101. h(VaporChild as any, {
  102. msg: msg.value,
  103. onVnodeMounted,
  104. })
  105. },
  106. }).render()
  107. expect(html()).toBe('foo|false')
  108. msg.value = 'bar'
  109. await nextTick()
  110. expect(html()).toBe('bar|false')
  111. })
  112. })
  113. describe('v-model', () => {
  114. test('basic work', async () => {
  115. const VaporChild = defineVaporComponent({
  116. props: {
  117. modelValue: {},
  118. modelModifiers: {},
  119. },
  120. emits: ['update:modelValue'],
  121. setup(__props) {
  122. const modelValue = useModel(__props, 'modelValue')
  123. const n0 = template('<h1> </h1>')() as any
  124. const n1 = template('<input>')() as any
  125. const x0 = child(n0) as any
  126. applyTextModel(
  127. n1,
  128. () => modelValue.value,
  129. _value => (modelValue.value = _value),
  130. )
  131. renderEffect(() => setText(x0, toDisplayString(modelValue.value)))
  132. return [n0, n1]
  133. },
  134. })
  135. const { html, host } = define({
  136. setup() {
  137. const msg = ref('foo')
  138. return () =>
  139. h(VaporChild as any, {
  140. modelValue: msg.value,
  141. 'onUpdate:modelValue': (value: string) => {
  142. msg.value = value
  143. },
  144. })
  145. },
  146. }).render()
  147. expect(html()).toBe('<h1>foo</h1><input>')
  148. const inputEl = host.querySelector('input')!
  149. inputEl.value = 'bar'
  150. inputEl.dispatchEvent(new Event('input'))
  151. await nextTick()
  152. expect(html()).toBe('<h1>bar</h1><input>')
  153. })
  154. test('slot v-model should persist when switching vapor/vdom child', async () => {
  155. const VaporComp1 = defineVaporComponent({
  156. name: 'VaporComp1',
  157. setup() {
  158. return [document.createTextNode('comp1: '), createSlot('default')]
  159. },
  160. })
  161. const VDomComp2 = defineComponent({
  162. name: 'VDomComp2',
  163. setup(_, { slots }) {
  164. return () =>
  165. h('div', [
  166. 'comp2: ',
  167. // vdom <slot/>
  168. renderSlot(slots, 'default'),
  169. ])
  170. },
  171. })
  172. const VaporParent = defineVaporComponent({
  173. name: 'VaporParent',
  174. props: {
  175. show: Boolean,
  176. modelValue: {},
  177. modelModifiers: {},
  178. },
  179. emits: ['update:modelValue'],
  180. setup(__props) {
  181. const modelValue = useModel(__props, 'modelValue')
  182. return createDynamicComponent(
  183. () => (__props.show ? VaporComp1 : VDomComp2),
  184. null,
  185. {
  186. default: () => {
  187. const input = template('<input>')() as any
  188. applyTextModel(
  189. input,
  190. () => modelValue.value,
  191. _value => (modelValue.value = _value),
  192. )
  193. return input
  194. },
  195. },
  196. true,
  197. )
  198. },
  199. })
  200. const show = ref(true)
  201. const msg = ref('')
  202. const { host } = define({
  203. setup() {
  204. return () =>
  205. h(VaporParent as any, {
  206. show: show.value,
  207. modelValue: msg.value,
  208. 'onUpdate:modelValue': (value: string) => {
  209. msg.value = value
  210. },
  211. })
  212. },
  213. }).render()
  214. const input1 = host.querySelector('input')!
  215. input1.value = 'hello'
  216. input1.dispatchEvent(new Event('input'))
  217. await nextTick()
  218. expect(msg.value).toBe('hello')
  219. show.value = false
  220. await nextTick()
  221. const input2 = host.querySelector('input')!
  222. expect(input2.value).toBe('hello')
  223. })
  224. })
  225. describe('emit', () => {
  226. test('emit from vapor child to vdom parent', () => {
  227. const VaporChild = defineVaporComponent({
  228. emits: ['click'],
  229. setup(_, { emit }) {
  230. emit('click')
  231. return []
  232. },
  233. })
  234. const fn = vi.fn()
  235. define({
  236. setup() {
  237. return () => h(VaporChild as any, { onClick: fn })
  238. },
  239. }).render()
  240. // fn should be called once
  241. expect(fn).toHaveBeenCalledTimes(1)
  242. })
  243. })
  244. describe('directives', () => {
  245. test('apply v-show to vdom child', async () => {
  246. const VDomChild = {
  247. setup() {
  248. return () => h('div')
  249. },
  250. }
  251. const show = ref(false)
  252. const VaporChild = defineVaporComponent({
  253. setup() {
  254. const n1 = createComponent(VDomChild as any)
  255. applyVShow(n1, () => show.value)
  256. return n1
  257. },
  258. })
  259. const { html } = define({
  260. setup() {
  261. return () => h(VaporChild as any)
  262. },
  263. }).render()
  264. expect(html()).toBe('<div style="display: none;"></div>')
  265. show.value = true
  266. await nextTick()
  267. expect(html()).toBe('<div style=""></div>')
  268. })
  269. test('apply v-show to vapor child', async () => {
  270. const VaporChild = defineVaporComponent({
  271. setup() {
  272. return template('<div></div>', true)()
  273. },
  274. })
  275. const show = ref(false)
  276. const App = defineComponent({
  277. setup() {
  278. return () =>
  279. h('div', null, [
  280. withDirectives(h(VaporChild as any), [[vShow, show.value]]),
  281. ])
  282. },
  283. })
  284. const root = document.createElement('div')
  285. const app = createApp(App)
  286. app.use(vaporInteropPlugin)
  287. app.mount(root)
  288. expect(root.innerHTML).toBe(
  289. '<div><div style="display: none;"></div></div>',
  290. )
  291. show.value = true
  292. await nextTick()
  293. expect(root.innerHTML).toBe('<div><div style=""></div></div>')
  294. })
  295. test('apply custom directive to vapor child', async () => {
  296. const vCustom = {
  297. created: vi.fn(),
  298. beforeMount: vi.fn(),
  299. mounted: vi.fn(),
  300. beforeUpdate: vi.fn(),
  301. updated: vi.fn(),
  302. beforeUnmount: vi.fn(),
  303. unmounted: vi.fn(),
  304. }
  305. const VaporChild = defineVaporComponent({
  306. setup() {
  307. return template('<div></div>', true)()
  308. },
  309. })
  310. const count = ref(0)
  311. const App = defineComponent({
  312. setup() {
  313. return () =>
  314. h('div', null, [
  315. withDirectives(h(VaporChild as any), [[vCustom, count.value]]),
  316. ])
  317. },
  318. })
  319. const root = document.createElement('div')
  320. const app = createApp(App)
  321. app.use(vaporInteropPlugin)
  322. app.mount(root)
  323. // root > div (App root) > div (VaporChild root)
  324. const el = root.querySelector('div')!.querySelector('div')!
  325. expect(vCustom.created).toHaveBeenCalledTimes(1)
  326. expect(vCustom.beforeMount).toHaveBeenCalledTimes(1)
  327. expect(vCustom.mounted).toHaveBeenCalledTimes(1)
  328. expect(vCustom.beforeUpdate).toHaveBeenCalledTimes(0)
  329. expect(vCustom.updated).toHaveBeenCalledTimes(0)
  330. expect(vCustom.created).toHaveBeenCalledWith(
  331. el,
  332. expect.objectContaining({ value: 0, oldValue: undefined }),
  333. expect.any(Object),
  334. null,
  335. )
  336. expect(vCustom.beforeMount).toHaveBeenCalledWith(
  337. el,
  338. expect.objectContaining({ value: 0, oldValue: undefined }),
  339. expect.any(Object),
  340. null,
  341. )
  342. expect(vCustom.mounted).toHaveBeenCalledWith(
  343. el,
  344. expect.objectContaining({ value: 0, oldValue: undefined }),
  345. expect.any(Object),
  346. null,
  347. )
  348. count.value++
  349. await nextTick()
  350. expect(vCustom.beforeUpdate).toHaveBeenCalledTimes(1)
  351. expect(vCustom.updated).toHaveBeenCalledTimes(1)
  352. expect(vCustom.beforeUpdate).toHaveBeenCalledWith(
  353. el,
  354. expect.objectContaining({ value: 1, oldValue: 0 }),
  355. expect.any(Object),
  356. expect.any(Object),
  357. )
  358. expect(vCustom.updated).toHaveBeenCalledWith(
  359. el,
  360. expect.objectContaining({ value: 1, oldValue: 0 }),
  361. expect.any(Object),
  362. expect.any(Object),
  363. )
  364. app.unmount()
  365. expect(vCustom.beforeUnmount).toHaveBeenCalledTimes(1)
  366. expect(vCustom.unmounted).toHaveBeenCalledTimes(1)
  367. expect(vCustom.beforeUnmount).toHaveBeenCalledWith(
  368. el,
  369. expect.objectContaining({ value: 1, oldValue: 0 }),
  370. expect.any(Object),
  371. null,
  372. )
  373. expect(vCustom.unmounted).toHaveBeenCalledWith(
  374. el,
  375. expect.objectContaining({ value: 1, oldValue: 0 }),
  376. expect.any(Object),
  377. null,
  378. )
  379. })
  380. test('warn on directive with non-element root vapor child', () => {
  381. const calls: string[] = []
  382. const vCustom = {
  383. created: () => calls.push('created'),
  384. beforeMount: () => calls.push('beforeMount'),
  385. mounted: () => calls.push('mounted'),
  386. beforeUpdate: () => calls.push('beforeUpdate'),
  387. updated: () => calls.push('updated'),
  388. beforeUnmount: () => calls.push('beforeUnmount'),
  389. unmounted: () => calls.push('unmounted'),
  390. }
  391. const VaporChild = defineVaporComponent({
  392. setup() {
  393. return [template('<div></div>')(), template('<div></div>')()]
  394. },
  395. })
  396. const App = defineComponent({
  397. setup() {
  398. return () =>
  399. h('div', null, [withDirectives(h(VaporChild as any), [[vCustom]])])
  400. },
  401. })
  402. const root = document.createElement('div')
  403. const app = createApp(App)
  404. app.use(vaporInteropPlugin)
  405. app.mount(root)
  406. if (__DEV__) {
  407. expect(
  408. `Runtime directive used on component with non-element root node.`,
  409. ).toHaveBeenWarned()
  410. }
  411. expect(calls.length).toBe(0)
  412. app.unmount()
  413. })
  414. })
  415. describe('slots', () => {
  416. test('basic', () => {
  417. const VDomChild = defineComponent({
  418. setup(_, { slots }) {
  419. return () => renderSlot(slots, 'default')
  420. },
  421. })
  422. const VaporChild = defineVaporComponent({
  423. setup() {
  424. return createComponent(
  425. VDomChild as any,
  426. null,
  427. {
  428. default: () => document.createTextNode('default slot'),
  429. },
  430. true,
  431. )
  432. },
  433. })
  434. const { html } = define({
  435. setup() {
  436. return () => h(VaporChild as any)
  437. },
  438. }).render()
  439. expect(html()).toBe('default slot')
  440. })
  441. test('functional slot', () => {
  442. const VDomChild = defineComponent({
  443. setup(_, { slots }) {
  444. return () => createVNode(slots.default!)
  445. },
  446. })
  447. const VaporChild = defineVaporComponent({
  448. setup() {
  449. return createComponent(
  450. VDomChild as any,
  451. null,
  452. {
  453. default: () => document.createTextNode('default slot'),
  454. },
  455. true,
  456. )
  457. },
  458. })
  459. const { html } = define({
  460. setup() {
  461. return () => h(VaporChild as any)
  462. },
  463. }).render()
  464. expect(html()).toBe('default slot')
  465. })
  466. test('slots.default() direct invocation', () => {
  467. const VDomChild = defineComponent({
  468. setup(_, { slots }) {
  469. return () => h('div', null, slots.default!())
  470. },
  471. })
  472. const VaporChild = defineVaporComponent({
  473. setup() {
  474. return createComponent(
  475. VDomChild as any,
  476. null,
  477. {
  478. default: () => template('direct call slot')(),
  479. },
  480. true,
  481. )
  482. },
  483. })
  484. const { html } = define({
  485. setup() {
  486. return () => h(VaporChild as any)
  487. },
  488. }).render()
  489. expect(html()).toBe('<div>direct call slot</div>')
  490. })
  491. test('slots.default() with slot props', () => {
  492. const VDomChild = defineComponent({
  493. setup(_, { slots }) {
  494. return () => h('div', null, slots.default!({ msg: 'hello' }))
  495. },
  496. })
  497. const VaporChild = defineVaporComponent({
  498. setup() {
  499. return createComponent(
  500. VDomChild as any,
  501. null,
  502. {
  503. default: (props: { msg: string }) => {
  504. const n0 = template('<span></span>')()
  505. n0.textContent = props.msg
  506. return [n0]
  507. },
  508. },
  509. true,
  510. )
  511. },
  512. })
  513. const { html } = define({
  514. setup() {
  515. return () => h(VaporChild as any)
  516. },
  517. }).render()
  518. expect(html()).toBe('<div><span>hello</span></div>')
  519. })
  520. test('slots.default() with falsy slot props should keep has/ownKeys semantics', () => {
  521. const VDomChild = defineComponent({
  522. setup(_, { slots }) {
  523. return () =>
  524. h('div', null, slots.default!({ flag: false, count: 0, text: '' }))
  525. },
  526. })
  527. const VaporChild = defineVaporComponent({
  528. setup() {
  529. return createComponent(
  530. VDomChild as any,
  531. null,
  532. {
  533. default: (props: Record<string, any>) => {
  534. const n0 = document.createTextNode(
  535. `${'flag' in props}/${'count' in props}/${'text' in props}|` +
  536. `${Object.keys(props).join(',')}|` +
  537. `${String(props.flag)},${String(props.count)},${String(props.text)}`,
  538. )
  539. return [n0]
  540. },
  541. },
  542. true,
  543. )
  544. },
  545. })
  546. const { html } = define({
  547. setup() {
  548. return () => h(VaporChild as any)
  549. },
  550. }).render()
  551. expect(html()).toBe('<div>true/true/true|flag,count,text|false,0,</div>')
  552. })
  553. test('named slot with slots[name]() invocation', () => {
  554. const VDomChild = defineComponent({
  555. setup(_, { slots }) {
  556. return () =>
  557. h('div', null, [
  558. h('header', null, slots.header!()),
  559. h('main', null, slots.default!()),
  560. h('footer', null, slots.footer!()),
  561. ])
  562. },
  563. })
  564. const VaporChild = defineVaporComponent({
  565. setup() {
  566. return createComponent(
  567. VDomChild as any,
  568. null,
  569. {
  570. header: () => template('Header')(),
  571. default: () => template('Main')(),
  572. footer: () => template('Footer')(),
  573. },
  574. true,
  575. )
  576. },
  577. })
  578. const { html } = define({
  579. setup() {
  580. return () => h(VaporChild as any)
  581. },
  582. }).render()
  583. expect(html()).toBe(
  584. '<div><header>Header</header><main>Main</main><footer>Footer</footer></div>',
  585. )
  586. })
  587. test('slots.default() return directly', () => {
  588. const VDomChild = defineComponent({
  589. setup(_, { slots }) {
  590. return () => slots.default!()
  591. },
  592. })
  593. const VaporChild = defineVaporComponent({
  594. setup() {
  595. return createComponent(
  596. VDomChild as any,
  597. null,
  598. {
  599. default: () => template('direct return slot')(),
  600. },
  601. true,
  602. )
  603. },
  604. })
  605. const { html } = define({
  606. setup() {
  607. return () => h(VaporChild as any)
  608. },
  609. }).render()
  610. expect(html()).toBe('direct return slot')
  611. })
  612. test('rendering forwarding vapor slot', () => {
  613. const VDomChild = defineComponent({
  614. setup(_, { slots }) {
  615. return () => h('div', null, { default: slots.default })
  616. },
  617. })
  618. const VaporChild = defineVaporComponent({
  619. setup() {
  620. return createComponent(
  621. VDomChild as any,
  622. null,
  623. {
  624. default: () => template('forwarded slot')(),
  625. },
  626. true,
  627. )
  628. },
  629. })
  630. const { html } = define({
  631. setup() {
  632. return () => h(VaporChild as any)
  633. },
  634. }).render()
  635. expect(html()).toBe('<div>forwarded slot</div>')
  636. })
  637. })
  638. describe('provide / inject', () => {
  639. it('should inject value from vdom parent', async () => {
  640. const VaporChild = defineVaporComponent({
  641. setup() {
  642. const foo = inject('foo')
  643. const n0 = template(' ')() as any
  644. renderEffect(() => setText(n0, toDisplayString(foo)))
  645. return n0
  646. },
  647. })
  648. const value = ref('foo')
  649. const { html } = define({
  650. setup() {
  651. provide('foo', value)
  652. return () => h(VaporChild as any)
  653. },
  654. }).render()
  655. expect(html()).toBe('foo')
  656. value.value = 'bar'
  657. await nextTick()
  658. expect(html()).toBe('bar')
  659. })
  660. })
  661. describe('template ref', () => {
  662. it('useTemplateRef with vapor child', async () => {
  663. const VaporChild = defineVaporComponent({
  664. setup(_, { expose }) {
  665. const foo = ref('foo')
  666. expose({ foo })
  667. const n0 = template(' ')() as any
  668. renderEffect(() => setText(n0, toDisplayString(foo)))
  669. return n0
  670. },
  671. })
  672. let elRef: ShallowRef
  673. const { html } = define({
  674. setup() {
  675. elRef = useTemplateRef('el')
  676. return () => h(VaporChild as any, { ref: 'el' })
  677. },
  678. }).render()
  679. expect(html()).toBe('foo')
  680. elRef!.value.foo = 'bar'
  681. await nextTick()
  682. expect(html()).toBe('bar')
  683. })
  684. it('static ref with vapor child', async () => {
  685. const VaporChild = defineVaporComponent({
  686. setup(_, { expose }) {
  687. const foo = ref('foo')
  688. expose({ foo })
  689. const n0 = template(' ')() as any
  690. renderEffect(() => setText(n0, toDisplayString(foo)))
  691. return n0
  692. },
  693. })
  694. let elRef: ShallowRef
  695. const { html } = define({
  696. setup() {
  697. elRef = shallowRef()
  698. return { elRef }
  699. },
  700. render() {
  701. return h(VaporChild as any, { ref: 'elRef' })
  702. },
  703. }).render()
  704. expect(html()).toBe('foo')
  705. elRef!.value.foo = 'bar'
  706. await nextTick()
  707. expect(html()).toBe('bar')
  708. })
  709. it('dynamic component includes vdom component', async () => {
  710. const vdomRef = ref<any>(null)
  711. const VdomChild = defineComponent({
  712. setup(_, { expose }) {
  713. expose({ name: 'vdomChild' })
  714. return () => h('div', 'vdom child')
  715. },
  716. })
  717. const VaporChild = defineVaporComponent({
  718. setup() {
  719. return { vdomRef }
  720. },
  721. render() {
  722. const setRef = createTemplateRefSetter()
  723. const n0 = createDynamicComponent(() => VdomChild)
  724. setRef(n0, vdomRef, false, 'vdomRef')
  725. return n0
  726. },
  727. })
  728. define({
  729. setup() {
  730. return () => h(VaporChild as any)
  731. },
  732. }).render()
  733. await nextTick()
  734. expect(vdomRef.value).toBeDefined()
  735. expect(vdomRef.value.name).toBe('vdomChild')
  736. })
  737. it('dynamic component includes vdom component should cleanup old ref', async () => {
  738. const VdomChild = defineComponent({
  739. setup(_, { expose }) {
  740. expose({ name: 'vdomChild' })
  741. return () => h('div', 'vdom child')
  742. },
  743. })
  744. const useA = ref(true)
  745. const refA = ref<any>(null)
  746. const refB = ref<any>(null)
  747. const VaporChild = defineVaporComponent({
  748. setup() {
  749. const setRef = createTemplateRefSetter()
  750. const n0 = createDynamicComponent(() => VdomChild)
  751. renderEffect(() => {
  752. setRef(n0, useA.value ? refA : refB, false, 'vdomRef')
  753. })
  754. return n0
  755. },
  756. })
  757. define({
  758. setup() {
  759. return () => h(VaporChild as any)
  760. },
  761. }).render()
  762. await nextTick()
  763. expect(refA.value).toBeDefined()
  764. expect(refA.value.name).toBe('vdomChild')
  765. expect(refB.value).toBe(null)
  766. useA.value = false
  767. await nextTick()
  768. expect(refA.value).toBe(null)
  769. expect(refB.value).toBeDefined()
  770. expect(refB.value.name).toBe('vdomChild')
  771. })
  772. })
  773. describe('dynamic component', () => {
  774. it('dynamic component with vapor child', async () => {
  775. const VaporChild = defineVaporComponent({
  776. setup() {
  777. return template('<div>vapor child</div>')() as any
  778. },
  779. })
  780. const VdomChild = defineComponent({
  781. setup() {
  782. return () => h('div', 'vdom child')
  783. },
  784. })
  785. const view = shallowRef<any>(VaporChild)
  786. const { html } = define({
  787. setup() {
  788. return () => h(resolveDynamicComponent(view.value) as any)
  789. },
  790. }).render()
  791. expect(html()).toBe('<div>vapor child</div>')
  792. view.value = VdomChild
  793. await nextTick()
  794. expect(html()).toBe('<div>vdom child</div>')
  795. view.value = VaporChild
  796. await nextTick()
  797. expect(html()).toBe('<div>vapor child</div>')
  798. })
  799. describe('render VNodes', () => {
  800. it('should render VNode containing vapor component from VDOM slot', async () => {
  801. const VaporComp = defineVaporComponent({
  802. setup() {
  803. return template('<div>vapor comp</div>')() as any
  804. },
  805. })
  806. const RouterView = defineComponent({
  807. setup(_, { slots }) {
  808. return () => {
  809. const component = h(VaporComp as any)
  810. return slots.default!({ Component: component })
  811. }
  812. },
  813. })
  814. const App = defineVaporComponent({
  815. setup() {
  816. return createComponent(
  817. RouterView as any,
  818. null,
  819. {
  820. default: (slotProps: { Component: any }) => {
  821. return createDynamicComponent(() => slotProps.Component)
  822. },
  823. },
  824. true,
  825. )
  826. },
  827. })
  828. const { html } = define({
  829. setup() {
  830. return () => h(App as any)
  831. },
  832. }).render()
  833. expect(html()).toBe('<div>vapor comp</div><!--dynamic-component-->')
  834. })
  835. it('should render VNode containing vdom component from VDOM slot', async () => {
  836. const VdomComp = defineComponent({
  837. setup() {
  838. return () => h('div', 'vdom comp')
  839. },
  840. })
  841. const RouterView = defineComponent({
  842. setup(_, { slots }) {
  843. return () => {
  844. const component = h(VdomComp)
  845. return slots.default!({ Component: component })
  846. }
  847. },
  848. })
  849. const App = defineVaporComponent({
  850. setup() {
  851. return createComponent(
  852. RouterView as any,
  853. null,
  854. {
  855. default: (slotProps: { Component: any }) => {
  856. return createDynamicComponent(() => slotProps.Component)
  857. },
  858. },
  859. true,
  860. )
  861. },
  862. })
  863. const { html } = define({
  864. setup() {
  865. return () => h(App as any)
  866. },
  867. }).render()
  868. expect(html()).toBe('<div>vdom comp</div><!--dynamic-component-->')
  869. })
  870. it('should update when VNode changes', async () => {
  871. const VaporCompA = defineVaporComponent({
  872. setup() {
  873. return template('<div>vapor A</div>')() as any
  874. },
  875. })
  876. const VaporCompB = defineVaporComponent({
  877. setup() {
  878. return template('<div>vapor B</div>')() as any
  879. },
  880. })
  881. const current = shallowRef<any>(VaporCompA)
  882. const RouterView = defineComponent({
  883. setup(_, { slots }) {
  884. return () => {
  885. const component = h(current.value as any)
  886. return slots.default!({ Component: component })
  887. }
  888. },
  889. })
  890. const App = defineVaporComponent({
  891. setup() {
  892. return createComponent(
  893. RouterView as any,
  894. null,
  895. {
  896. default: (slotProps: { Component: any }) => {
  897. return createDynamicComponent(() => slotProps.Component)
  898. },
  899. },
  900. true,
  901. )
  902. },
  903. })
  904. const { html } = define({
  905. setup() {
  906. return () => h(App as any)
  907. },
  908. }).render()
  909. expect(html()).toBe('<div>vapor A</div><!--dynamic-component-->')
  910. current.value = VaporCompB
  911. await nextTick()
  912. expect(html()).toBe('<div>vapor B</div><!--dynamic-component-->')
  913. })
  914. describe('with VaporKeepAlive', () => {
  915. it('switch VNode with inner vapor components', async () => {
  916. const hooksA = {
  917. mounted: vi.fn(),
  918. activated: vi.fn(),
  919. deactivated: vi.fn(),
  920. unmounted: vi.fn(),
  921. }
  922. const hooksB = {
  923. mounted: vi.fn(),
  924. activated: vi.fn(),
  925. deactivated: vi.fn(),
  926. unmounted: vi.fn(),
  927. }
  928. const VaporCompA = defineVaporComponent({
  929. setup() {
  930. onMounted(() => hooksA.mounted())
  931. onActivated(() => hooksA.activated())
  932. onDeactivated(() => hooksA.deactivated())
  933. onUnmounted(() => hooksA.unmounted())
  934. return template('<div>vapor A</div>')() as any
  935. },
  936. })
  937. const VaporCompB = defineVaporComponent({
  938. setup() {
  939. onMounted(() => hooksB.mounted())
  940. onActivated(() => hooksB.activated())
  941. onDeactivated(() => hooksB.deactivated())
  942. onUnmounted(() => hooksB.unmounted())
  943. return template('<div>vapor B</div>')() as any
  944. },
  945. })
  946. const current = shallowRef<any>(VaporCompA)
  947. const RouterView = defineComponent({
  948. setup(_, { slots }) {
  949. return () => {
  950. const component = h(current.value as any)
  951. return slots.default!({ Component: component })
  952. }
  953. },
  954. })
  955. const App = defineVaporComponent({
  956. setup() {
  957. return createComponent(
  958. RouterView as any,
  959. null,
  960. {
  961. default: (slotProps: { Component: any }) => {
  962. return createComponent(VaporKeepAlive, null, {
  963. default: () =>
  964. createDynamicComponent(() => slotProps.Component),
  965. })
  966. },
  967. },
  968. true,
  969. )
  970. },
  971. })
  972. const { html } = define({
  973. setup() {
  974. return () => h(App as any)
  975. },
  976. }).render()
  977. expect(html()).toBe('<div>vapor A</div><!--dynamic-component-->')
  978. // A: mounted + activated
  979. expect(hooksA.mounted).toHaveBeenCalledTimes(1)
  980. expect(hooksA.activated).toHaveBeenCalledTimes(1)
  981. expect(hooksA.deactivated).toHaveBeenCalledTimes(0)
  982. expect(hooksA.unmounted).toHaveBeenCalledTimes(0)
  983. current.value = VaporCompB
  984. await nextTick()
  985. expect(html()).toBe('<div>vapor B</div><!--dynamic-component-->')
  986. // A: deactivated (cached)
  987. expect(hooksA.deactivated).toHaveBeenCalledTimes(1)
  988. expect(hooksA.unmounted).toHaveBeenCalledTimes(0)
  989. // B: mounted + activated
  990. expect(hooksB.mounted).toHaveBeenCalledTimes(1)
  991. expect(hooksB.activated).toHaveBeenCalledTimes(1)
  992. current.value = VaporCompA
  993. await nextTick()
  994. expect(html()).toBe('<div>vapor A</div><!--dynamic-component-->')
  995. // B: deactivated (cached)
  996. expect(hooksB.deactivated).toHaveBeenCalledTimes(1)
  997. expect(hooksB.unmounted).toHaveBeenCalledTimes(0)
  998. // A: re-activated (not re-mounted)
  999. expect(hooksA.mounted).toHaveBeenCalledTimes(1)
  1000. expect(hooksA.activated).toHaveBeenCalledTimes(2)
  1001. })
  1002. it('switch VNode with inner VDOM components', async () => {
  1003. const hooksA = {
  1004. mounted: vi.fn(),
  1005. activated: vi.fn(),
  1006. deactivated: vi.fn(),
  1007. unmounted: vi.fn(),
  1008. }
  1009. const hooksB = {
  1010. mounted: vi.fn(),
  1011. activated: vi.fn(),
  1012. deactivated: vi.fn(),
  1013. unmounted: vi.fn(),
  1014. }
  1015. const VDOMCompA = defineComponent({
  1016. setup() {
  1017. onMounted(() => hooksA.mounted())
  1018. onActivated(() => hooksA.activated())
  1019. onDeactivated(() => hooksA.deactivated())
  1020. onUnmounted(() => hooksA.unmounted())
  1021. return () => h('div', 'vdom A')
  1022. },
  1023. })
  1024. const VDOMCompB = defineComponent({
  1025. setup() {
  1026. onMounted(() => hooksB.mounted())
  1027. onActivated(() => hooksB.activated())
  1028. onDeactivated(() => hooksB.deactivated())
  1029. onUnmounted(() => hooksB.unmounted())
  1030. return () => h('div', 'vdom B')
  1031. },
  1032. })
  1033. const current = shallowRef<any>(VDOMCompA)
  1034. const RouterView = defineComponent({
  1035. setup(_, { slots }) {
  1036. return () => {
  1037. const component = h(current.value as any)
  1038. return slots.default!({ Component: component })
  1039. }
  1040. },
  1041. })
  1042. const App = defineVaporComponent({
  1043. setup() {
  1044. return createComponent(
  1045. RouterView as any,
  1046. null,
  1047. {
  1048. default: (slotProps: { Component: any }) => {
  1049. return createComponent(VaporKeepAlive, null, {
  1050. default: () =>
  1051. createDynamicComponent(() => slotProps.Component),
  1052. })
  1053. },
  1054. },
  1055. true,
  1056. )
  1057. },
  1058. })
  1059. const { html } = define({
  1060. setup() {
  1061. return () => h(App as any)
  1062. },
  1063. }).render()
  1064. expect(html()).toBe('<div>vdom A</div><!--dynamic-component-->')
  1065. // A: mounted + activated
  1066. expect(hooksA.mounted).toHaveBeenCalledTimes(1)
  1067. expect(hooksA.activated).toHaveBeenCalledTimes(1)
  1068. expect(hooksA.deactivated).toHaveBeenCalledTimes(0)
  1069. expect(hooksA.unmounted).toHaveBeenCalledTimes(0)
  1070. current.value = VDOMCompB
  1071. await nextTick()
  1072. expect(html()).toBe('<div>vdom B</div><!--dynamic-component-->')
  1073. // A: deactivated (cached)
  1074. expect(hooksA.deactivated).toHaveBeenCalledTimes(1)
  1075. expect(hooksA.unmounted).toHaveBeenCalledTimes(0)
  1076. // B: mounted + activated
  1077. expect(hooksB.mounted).toHaveBeenCalledTimes(1)
  1078. expect(hooksB.activated).toHaveBeenCalledTimes(1)
  1079. current.value = VDOMCompA
  1080. await nextTick()
  1081. expect(html()).toBe('<div>vdom A</div><!--dynamic-component-->')
  1082. // B: deactivated (cached)
  1083. expect(hooksB.deactivated).toHaveBeenCalledTimes(1)
  1084. expect(hooksB.unmounted).toHaveBeenCalledTimes(0)
  1085. // A: re-activated (not re-mounted)
  1086. expect(hooksA.mounted).toHaveBeenCalledTimes(1)
  1087. expect(hooksA.activated).toHaveBeenCalledTimes(2)
  1088. })
  1089. it('switch VNode with inner mixed vapor/VDOM components', async () => {
  1090. const hooksA = {
  1091. mounted: vi.fn(),
  1092. activated: vi.fn(),
  1093. deactivated: vi.fn(),
  1094. unmounted: vi.fn(),
  1095. }
  1096. const hooksB = {
  1097. mounted: vi.fn(),
  1098. activated: vi.fn(),
  1099. deactivated: vi.fn(),
  1100. unmounted: vi.fn(),
  1101. }
  1102. const VaporCompA = defineVaporComponent({
  1103. setup() {
  1104. onMounted(() => hooksA.mounted())
  1105. onActivated(() => hooksA.activated())
  1106. onDeactivated(() => hooksA.deactivated())
  1107. onUnmounted(() => hooksA.unmounted())
  1108. return template('<div>vapor A</div>')()
  1109. },
  1110. })
  1111. const VDOMCompB = defineComponent({
  1112. setup() {
  1113. onMounted(() => hooksB.mounted())
  1114. onActivated(() => hooksB.activated())
  1115. onDeactivated(() => hooksB.deactivated())
  1116. onUnmounted(() => hooksB.unmounted())
  1117. return () => h('div', 'vdom B')
  1118. },
  1119. })
  1120. const current = shallowRef<any>(VaporCompA)
  1121. const RouterView = defineComponent({
  1122. setup(_, { slots }) {
  1123. return () => {
  1124. const component = h(current.value as any)
  1125. return slots.default!({ Component: component })
  1126. }
  1127. },
  1128. })
  1129. const App = defineVaporComponent({
  1130. setup() {
  1131. return createComponent(
  1132. RouterView as any,
  1133. null,
  1134. {
  1135. default: (slotProps: { Component: any }) => {
  1136. return createComponent(VaporKeepAlive, null, {
  1137. default: () =>
  1138. createDynamicComponent(() => slotProps.Component),
  1139. })
  1140. },
  1141. },
  1142. true,
  1143. )
  1144. },
  1145. })
  1146. const { html } = define({
  1147. setup() {
  1148. return () => h(App as any)
  1149. },
  1150. }).render()
  1151. expect(html()).toBe('<div>vapor A</div><!--dynamic-component-->')
  1152. // A (vapor): mounted + activated
  1153. expect(hooksA.mounted).toHaveBeenCalledTimes(1)
  1154. expect(hooksA.activated).toHaveBeenCalledTimes(1)
  1155. expect(hooksA.deactivated).toHaveBeenCalledTimes(0)
  1156. expect(hooksA.unmounted).toHaveBeenCalledTimes(0)
  1157. current.value = VDOMCompB
  1158. await nextTick()
  1159. expect(html()).toBe('<div>vdom B</div><!--dynamic-component-->')
  1160. // A (vapor): deactivated (cached)
  1161. expect(hooksA.deactivated).toHaveBeenCalledTimes(1)
  1162. expect(hooksA.unmounted).toHaveBeenCalledTimes(0)
  1163. // B (vdom): mounted + activated
  1164. expect(hooksB.mounted).toHaveBeenCalledTimes(1)
  1165. expect(hooksB.activated).toHaveBeenCalledTimes(1)
  1166. current.value = VaporCompA
  1167. await nextTick()
  1168. expect(html()).toBe('<div>vapor A</div><!--dynamic-component-->')
  1169. // B (vdom): deactivated (cached)
  1170. expect(hooksB.deactivated).toHaveBeenCalledTimes(1)
  1171. expect(hooksB.unmounted).toHaveBeenCalledTimes(0)
  1172. // A (vapor): re-activated (not re-mounted)
  1173. expect(hooksA.mounted).toHaveBeenCalledTimes(1)
  1174. expect(hooksA.activated).toHaveBeenCalledTimes(2)
  1175. })
  1176. })
  1177. })
  1178. })
  1179. describe('attribute fallthrough', () => {
  1180. it('should fallthrough attrs to vdom child', () => {
  1181. const VDomChild = defineComponent({
  1182. setup() {
  1183. return () => h('div')
  1184. },
  1185. })
  1186. const VaporChild = defineVaporComponent({
  1187. setup() {
  1188. return createComponent(
  1189. VDomChild as any,
  1190. { foo: () => 'vapor foo' },
  1191. null,
  1192. true,
  1193. )
  1194. },
  1195. })
  1196. const { html } = define({
  1197. setup() {
  1198. return () => h(VaporChild as any, { foo: 'foo', bar: 'bar' })
  1199. },
  1200. }).render()
  1201. expect(html()).toBe('<div foo="foo" bar="bar"></div>')
  1202. })
  1203. it('should not fallthrough emit handlers to vdom child', () => {
  1204. const VDomChild = defineComponent({
  1205. emits: ['click'],
  1206. setup(_, { emit }) {
  1207. return () => h('button', { onClick: () => emit('click') }, 'click me')
  1208. },
  1209. })
  1210. const fn = vi.fn()
  1211. const VaporChild = defineVaporComponent({
  1212. emits: ['click'],
  1213. setup() {
  1214. return createComponent(
  1215. VDomChild as any,
  1216. { onClick: () => fn },
  1217. null,
  1218. true,
  1219. )
  1220. },
  1221. })
  1222. const { host, html } = define({
  1223. setup() {
  1224. return () => h(VaporChild as any)
  1225. },
  1226. }).render()
  1227. expect(html()).toBe('<button>click me</button>')
  1228. const button = host.querySelector('button')!
  1229. button.dispatchEvent(new Event('click'))
  1230. // fn should be called once
  1231. expect(fn).toHaveBeenCalledTimes(1)
  1232. })
  1233. it('should update attrs passed from vapor parent to vdom child', async () => {
  1234. const msg = ref('foo')
  1235. const VDomChild = defineComponent({
  1236. setup(_, { attrs }) {
  1237. return () =>
  1238. h(
  1239. 'div',
  1240. {
  1241. 'data-msg': attrs['data-msg'] as string,
  1242. },
  1243. attrs['data-msg'] as string,
  1244. )
  1245. },
  1246. })
  1247. const VaporChild = defineVaporComponent({
  1248. setup() {
  1249. return createComponent(
  1250. VDomChild as any,
  1251. {
  1252. 'data-msg': () => msg.value,
  1253. },
  1254. null,
  1255. true,
  1256. )
  1257. },
  1258. })
  1259. const { html } = define({
  1260. setup() {
  1261. return () => h(VaporChild as any)
  1262. },
  1263. }).render()
  1264. expect(html()).toBe('<div data-msg="foo">foo</div>')
  1265. msg.value = 'bar'
  1266. await nextTick()
  1267. expect(html()).toBe('<div data-msg="bar">bar</div>')
  1268. })
  1269. })
  1270. describe('async component', () => {
  1271. const duration = 5
  1272. test('render vapor async component', async () => {
  1273. const VdomChild = {
  1274. setup() {
  1275. return () => h('div', 'foo')
  1276. },
  1277. }
  1278. const VaporAsyncChild = defineVaporAsyncComponent({
  1279. loader: () => {
  1280. return new Promise(r => {
  1281. setTimeout(() => {
  1282. r(VdomChild as any)
  1283. }, duration)
  1284. })
  1285. },
  1286. loadingComponent: () => h('span', 'loading...'),
  1287. })
  1288. const { html } = define({
  1289. setup() {
  1290. return () => h(VaporAsyncChild as any)
  1291. },
  1292. }).render()
  1293. expect(html()).toBe('<span>loading...</span><!--async component-->')
  1294. await new Promise(r => setTimeout(r, duration))
  1295. await nextTick()
  1296. expect(html()).toBe('<div>foo</div><!--async component-->')
  1297. })
  1298. })
  1299. describe('keepalive', () => {
  1300. function assertHookCalls(
  1301. hooks: {
  1302. beforeMount: any
  1303. mounted: any
  1304. activated: any
  1305. deactivated: any
  1306. unmounted: any
  1307. },
  1308. callCounts: number[],
  1309. ) {
  1310. expect([
  1311. hooks.beforeMount.mock.calls.length,
  1312. hooks.mounted.mock.calls.length,
  1313. hooks.activated.mock.calls.length,
  1314. hooks.deactivated.mock.calls.length,
  1315. hooks.unmounted.mock.calls.length,
  1316. ]).toEqual(callCounts)
  1317. }
  1318. let hooks: any
  1319. beforeEach(() => {
  1320. hooks = {
  1321. beforeMount: vi.fn(),
  1322. mounted: vi.fn(),
  1323. activated: vi.fn(),
  1324. deactivated: vi.fn(),
  1325. unmounted: vi.fn(),
  1326. }
  1327. })
  1328. test('render vapor component', async () => {
  1329. const VaporChild = defineVaporComponent({
  1330. setup() {
  1331. const msg = ref('vapor')
  1332. onBeforeMount(() => hooks.beforeMount())
  1333. onMounted(() => hooks.mounted())
  1334. onActivated(() => hooks.activated())
  1335. onDeactivated(() => hooks.deactivated())
  1336. onUnmounted(() => hooks.unmounted())
  1337. const n0 = template('<input type="text">', true)() as any
  1338. applyTextModel(
  1339. n0,
  1340. () => msg.value,
  1341. _value => (msg.value = _value),
  1342. )
  1343. return n0
  1344. },
  1345. })
  1346. const show = ref(true)
  1347. const toggle = ref(true)
  1348. const { html, host } = define({
  1349. setup() {
  1350. return () =>
  1351. show.value
  1352. ? h(KeepAlive, null, {
  1353. default: () => (toggle.value ? h(VaporChild as any) : null),
  1354. })
  1355. : null
  1356. },
  1357. }).render()
  1358. expect(html()).toBe('<input type="text">')
  1359. let inputEl = host.firstChild as HTMLInputElement
  1360. expect(inputEl.value).toBe('vapor')
  1361. assertHookCalls(hooks, [1, 1, 1, 0, 0])
  1362. // change input value
  1363. inputEl.value = 'changed'
  1364. inputEl.dispatchEvent(new Event('input'))
  1365. await nextTick()
  1366. // deactivate
  1367. toggle.value = false
  1368. await nextTick()
  1369. expect(html()).toBe('<!---->')
  1370. assertHookCalls(hooks, [1, 1, 1, 1, 0])
  1371. // activate
  1372. toggle.value = true
  1373. await nextTick()
  1374. expect(html()).toBe('<input type="text">')
  1375. inputEl = host.firstChild as HTMLInputElement
  1376. expect(inputEl.value).toBe('changed')
  1377. assertHookCalls(hooks, [1, 1, 2, 1, 0])
  1378. // unmount keepalive
  1379. show.value = false
  1380. await nextTick()
  1381. expect(html()).toBe('<!---->')
  1382. assertHookCalls(hooks, [1, 1, 2, 2, 1])
  1383. // mount keepalive
  1384. show.value = true
  1385. await nextTick()
  1386. inputEl = host.firstChild as HTMLInputElement
  1387. expect(inputEl.value).toBe('vapor')
  1388. assertHookCalls(hooks, [2, 2, 3, 2, 1])
  1389. })
  1390. test('render vapor slot', async () => {
  1391. const show = ref(true)
  1392. const VDomComp = defineComponent({
  1393. setup(_, { slots }) {
  1394. return () => renderSlot(slots, 'default')
  1395. },
  1396. })
  1397. const App = defineVaporComponent({
  1398. setup() {
  1399. const n5 = createComponent(VaporKeepAlive, null, {
  1400. default: () =>
  1401. createIf(
  1402. () => show.value,
  1403. () =>
  1404. createComponent(VDomComp as any, null, {
  1405. default: () => template('slot text')(),
  1406. }),
  1407. ),
  1408. })
  1409. return n5
  1410. },
  1411. })
  1412. const { html } = define({
  1413. setup() {
  1414. return () => h(App)
  1415. },
  1416. }).render()
  1417. expect(html()).toBe('slot text<!--if-->')
  1418. show.value = false
  1419. await nextTick()
  1420. expect(html()).toBe('<!--if-->')
  1421. show.value = true
  1422. await nextTick()
  1423. expect(html()).toBe('slot text<!--if-->')
  1424. })
  1425. test('unmounting vapor slot should remove vnode slot content', async () => {
  1426. const show = ref(true)
  1427. const VaporSlotOutlet = defineVaporComponent({
  1428. setup() {
  1429. return createSlot('default')
  1430. },
  1431. })
  1432. const { html } = define({
  1433. setup() {
  1434. return () =>
  1435. h('div', null, [
  1436. show.value
  1437. ? h(VaporSlotOutlet as any, null, {
  1438. default: () => [h('span', 'slot vnode')],
  1439. })
  1440. : null,
  1441. ])
  1442. },
  1443. }).render()
  1444. expect(html()).toBe('<div><span>slot vnode</span></div>')
  1445. show.value = false
  1446. await nextTick()
  1447. expect(html()).toBe('<div><!----></div>')
  1448. })
  1449. })
  1450. describe('Teleport', () => {
  1451. test('mounts VDOM Teleport from createDynamicComponent', async () => {
  1452. const target = document.createElement('div')
  1453. target.id = 'interop-teleport-target'
  1454. document.body.appendChild(target)
  1455. try {
  1456. const VaporChild = defineVaporComponent({
  1457. setup() {
  1458. return createDynamicComponent(
  1459. () => Teleport,
  1460. { to: () => '#interop-teleport-target' },
  1461. {
  1462. default: () => template('<span>teleported</span>')(),
  1463. },
  1464. true,
  1465. )
  1466. },
  1467. })
  1468. define({
  1469. setup() {
  1470. return () => h(VaporChild as any)
  1471. },
  1472. }).render()
  1473. await nextTick()
  1474. expect(target.innerHTML).toContain('<span>teleported</span>')
  1475. } finally {
  1476. target.remove()
  1477. }
  1478. })
  1479. })
  1480. describe('Suspense', () => {
  1481. test('renders async vapor child inside VDOM Suspense', async () => {
  1482. const duration = 5
  1483. const VaporAsyncChild = defineVaporComponent({
  1484. async setup() {
  1485. await new Promise(resolve => setTimeout(resolve, duration))
  1486. return template('<div><button>click</button></div>')()
  1487. },
  1488. })
  1489. const VaporParent = defineVaporComponent({
  1490. setup() {
  1491. return createComponent(
  1492. Suspense as any,
  1493. null,
  1494. {
  1495. default: () => createComponent(VaporAsyncChild, null, null, true),
  1496. fallback: () => template('loading')(),
  1497. },
  1498. true,
  1499. )
  1500. },
  1501. })
  1502. const { html } = define({
  1503. setup() {
  1504. return () => h(VaporParent as any)
  1505. },
  1506. }).render()
  1507. expect(html()).toContain('loading')
  1508. await new Promise(resolve => setTimeout(resolve, duration + 1))
  1509. await nextTick()
  1510. expect(html()).toContain('<div><button>click</button></div>')
  1511. })
  1512. test('renders async VDOM child inside VDOM Suspense', async () => {
  1513. const duration = 5
  1514. const VDomAsyncChild = defineComponent({
  1515. async setup() {
  1516. await new Promise(resolve => setTimeout(resolve, duration))
  1517. return () => h('div', [h('button', 'click')])
  1518. },
  1519. })
  1520. const VaporParent = defineVaporComponent({
  1521. setup() {
  1522. return createComponent(
  1523. Suspense as any,
  1524. null,
  1525. {
  1526. default: () =>
  1527. createComponent(VDomAsyncChild as any, null, null, true),
  1528. fallback: () => template('loading')(),
  1529. },
  1530. true,
  1531. )
  1532. },
  1533. })
  1534. const { html } = define({
  1535. setup() {
  1536. return () => h(VaporParent as any)
  1537. },
  1538. }).render()
  1539. expect(html()).toContain('loading')
  1540. await new Promise(resolve => setTimeout(resolve, duration + 1))
  1541. await nextTick()
  1542. expect(html()).toContain('<div><button>click</button></div>')
  1543. })
  1544. test('renders async VDOM child from vapor slot outlet inside VDOM Suspense', async () => {
  1545. const duration = 5
  1546. const VaporSlotOutlet = defineVaporComponent({
  1547. setup() {
  1548. return createSlot('default')
  1549. },
  1550. })
  1551. const VDomAsyncChild = defineComponent({
  1552. async setup() {
  1553. await new Promise(resolve => setTimeout(resolve, duration))
  1554. return () => h('div', 'slot async')
  1555. },
  1556. })
  1557. const App = defineComponent({
  1558. setup() {
  1559. return () =>
  1560. h(Suspense, null, {
  1561. default: () =>
  1562. h(VaporSlotOutlet as any, null, {
  1563. default: () => [h(VDomAsyncChild as any)],
  1564. }),
  1565. fallback: () => h('div', 'loading'),
  1566. })
  1567. },
  1568. })
  1569. const { html } = define(App).render()
  1570. expect(html()).toContain('loading')
  1571. await new Promise(resolve => setTimeout(resolve, duration + 1))
  1572. await nextTick()
  1573. expect(html()).toContain('<div>slot async</div>')
  1574. })
  1575. test('renders async VDOM vnode via createDynamicComponent inside VDOM Suspense', async () => {
  1576. const duration = 5
  1577. const VDomAsyncChild = defineComponent({
  1578. async setup() {
  1579. await new Promise(resolve => setTimeout(resolve, duration))
  1580. return () => h('button', 'vnode async')
  1581. },
  1582. })
  1583. const VaporParent = defineVaporComponent({
  1584. setup() {
  1585. return createComponent(
  1586. Suspense as any,
  1587. null,
  1588. {
  1589. default: () =>
  1590. createDynamicComponent(
  1591. () => h(VDomAsyncChild as any),
  1592. null,
  1593. null,
  1594. true,
  1595. ),
  1596. fallback: () => template('loading')(),
  1597. },
  1598. true,
  1599. )
  1600. },
  1601. })
  1602. const { html } = define({
  1603. setup() {
  1604. return () => h(VaporParent as any)
  1605. },
  1606. }).render()
  1607. expect(html()).toContain('loading')
  1608. await new Promise(resolve => setTimeout(resolve, duration + 1))
  1609. await nextTick()
  1610. expect(html()).toContain('<button>vnode async</button>')
  1611. })
  1612. test('mounts VDOM Suspense from createDynamicComponent', async () => {
  1613. const VaporChild = defineVaporComponent({
  1614. setup() {
  1615. return createDynamicComponent(
  1616. () => Suspense,
  1617. null,
  1618. {
  1619. default: () => template('<span>resolved</span>')(),
  1620. fallback: () => template('<span>fallback</span>')(),
  1621. },
  1622. true,
  1623. )
  1624. },
  1625. })
  1626. const { html } = define({
  1627. setup() {
  1628. return () => h(VaporChild as any)
  1629. },
  1630. }).render()
  1631. await nextTick()
  1632. expect(html()).toContain('<span>resolved</span>')
  1633. })
  1634. })
  1635. })