apiDefineAsyncComponent.spec.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863
  1. import { nextTick, onActivated, ref } from '@vue/runtime-dom'
  2. import { type VaporComponent, createComponent } from '../src/component'
  3. import { defineVaporAsyncComponent } from '../src/apiDefineAsyncComponent'
  4. import { makeRender } from './_utils'
  5. import {
  6. VaporKeepAlive,
  7. createIf,
  8. createTemplateRefSetter,
  9. defineVaporComponent,
  10. renderEffect,
  11. template,
  12. withVaporCtx,
  13. } from '@vue/runtime-vapor'
  14. import { setElementText } from '../src/dom/prop'
  15. const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n))
  16. const define = makeRender()
  17. describe('api: defineAsyncComponent', () => {
  18. test('simple usage', async () => {
  19. let resolve: (comp: VaporComponent) => void
  20. const Foo = defineVaporAsyncComponent(
  21. () =>
  22. new Promise(r => {
  23. resolve = r as any
  24. }),
  25. )
  26. const toggle = ref(true)
  27. const { html } = define({
  28. setup() {
  29. return createIf(
  30. () => toggle.value,
  31. () => {
  32. return createComponent(Foo)
  33. },
  34. )
  35. },
  36. }).render()
  37. expect(html()).toBe('<!--async component--><!--if-->')
  38. resolve!(() => template('resolved')())
  39. await timeout()
  40. expect(html()).toBe('resolved<!--async component--><!--if-->')
  41. toggle.value = false
  42. await nextTick()
  43. expect(html()).toBe('<!--if-->')
  44. // already resolved component should update on nextTick
  45. toggle.value = true
  46. await nextTick()
  47. expect(html()).toBe('resolved<!--async component--><!--if-->')
  48. })
  49. test('with loading component', async () => {
  50. let resolve: (comp: VaporComponent) => void
  51. const Foo = defineVaporAsyncComponent({
  52. loader: () =>
  53. new Promise(r => {
  54. resolve = r as any
  55. }),
  56. loadingComponent: () => template('loading')(),
  57. delay: 1, // defaults to 200
  58. })
  59. const toggle = ref(true)
  60. const { html } = define({
  61. setup() {
  62. return createIf(
  63. () => toggle.value,
  64. () => {
  65. return createComponent(Foo)
  66. },
  67. )
  68. },
  69. }).render()
  70. // due to the delay, initial mount should be empty
  71. expect(html()).toBe('<!--async component--><!--if-->')
  72. // loading show up after delay
  73. await timeout(1)
  74. expect(html()).toBe('loading<!--async component--><!--if-->')
  75. resolve!(() => template('resolved')())
  76. await timeout()
  77. expect(html()).toBe('resolved<!--async component--><!--if-->')
  78. toggle.value = false
  79. await nextTick()
  80. expect(html()).toBe('<!--if-->')
  81. // already resolved component should update on nextTick without loading
  82. // state
  83. toggle.value = true
  84. await nextTick()
  85. expect(html()).toBe('resolved<!--async component--><!--if-->')
  86. })
  87. test('with loading component + explicit delay (0)', async () => {
  88. let resolve: (comp: VaporComponent) => void
  89. const Foo = defineVaporAsyncComponent({
  90. loader: () =>
  91. new Promise(r => {
  92. resolve = r as any
  93. }),
  94. loadingComponent: () => template('loading')(),
  95. delay: 0,
  96. })
  97. const toggle = ref(true)
  98. const { html } = define({
  99. setup() {
  100. return createIf(
  101. () => toggle.value,
  102. () => {
  103. return createComponent(Foo)
  104. },
  105. )
  106. },
  107. }).render()
  108. // with delay: 0, should show loading immediately
  109. expect(html()).toBe('loading<!--async component--><!--if-->')
  110. resolve!(() => template('resolved')())
  111. await timeout()
  112. expect(html()).toBe('resolved<!--async component--><!--if-->')
  113. toggle.value = false
  114. await nextTick()
  115. expect(html()).toBe('<!--if-->')
  116. // already resolved component should update on nextTick without loading
  117. // state
  118. toggle.value = true
  119. await nextTick()
  120. expect(html()).toBe('resolved<!--async component--><!--if-->')
  121. })
  122. test('error without error component', async () => {
  123. let resolve: (comp: VaporComponent) => void
  124. let reject: (e: Error) => void
  125. const Foo = defineVaporAsyncComponent(
  126. () =>
  127. new Promise((_resolve, _reject) => {
  128. resolve = _resolve as any
  129. reject = _reject
  130. }),
  131. )
  132. const toggle = ref(true)
  133. const { app, mount } = define({
  134. setup() {
  135. return createIf(
  136. () => toggle.value,
  137. () => {
  138. return createComponent(Foo)
  139. },
  140. )
  141. },
  142. }).create()
  143. const handler = (app.config.errorHandler = vi.fn())
  144. const root = document.createElement('div')
  145. mount(root)
  146. expect(root.innerHTML).toBe('<!--async component--><!--if-->')
  147. const err = new Error('foo')
  148. reject!(err)
  149. await timeout()
  150. expect(handler).toHaveBeenCalled()
  151. expect(handler.mock.calls[0][0]).toBe(err)
  152. expect(root.innerHTML).toBe('<!--async component--><!--if-->')
  153. toggle.value = false
  154. await nextTick()
  155. expect(root.innerHTML).toBe('<!--if-->')
  156. // errored out on previous load, toggle and mock success this time
  157. toggle.value = true
  158. await nextTick()
  159. expect(root.innerHTML).toBe('<!--async component--><!--if-->')
  160. // should render this time
  161. resolve!(() => template('resolved')())
  162. await timeout()
  163. expect(root.innerHTML).toBe('resolved<!--async component--><!--if-->')
  164. })
  165. test('error with error component', async () => {
  166. let resolve: (comp: VaporComponent) => void
  167. let reject: (e: Error) => void
  168. const Foo = defineVaporAsyncComponent({
  169. loader: () =>
  170. new Promise((_resolve, _reject) => {
  171. resolve = _resolve as any
  172. reject = _reject
  173. }),
  174. errorComponent: (props: { error: Error }) =>
  175. template(props.error.message)(),
  176. })
  177. const toggle = ref(true)
  178. const { app, mount } = define({
  179. setup() {
  180. return createIf(
  181. () => toggle.value,
  182. () => {
  183. return createComponent(Foo)
  184. },
  185. )
  186. },
  187. }).create()
  188. const handler = (app.config.errorHandler = vi.fn())
  189. const root = document.createElement('div')
  190. mount(root)
  191. expect(root.innerHTML).toBe('<!--async component--><!--if-->')
  192. const err = new Error('errored out')
  193. reject!(err)
  194. await timeout()
  195. expect(handler).toHaveBeenCalled()
  196. expect(root.innerHTML).toBe('errored out<!--async component--><!--if-->')
  197. toggle.value = false
  198. await nextTick()
  199. expect(root.innerHTML).toBe('<!--if-->')
  200. // errored out on previous load, toggle and mock success this time
  201. toggle.value = true
  202. await nextTick()
  203. expect(root.innerHTML).toBe('<!--async component--><!--if-->')
  204. // should render this time
  205. resolve!(() => template('resolved')())
  206. await timeout()
  207. expect(root.innerHTML).toBe('resolved<!--async component--><!--if-->')
  208. })
  209. test('error with error component, without global handler', async () => {
  210. let resolve: (comp: VaporComponent) => void
  211. let reject: (e: Error) => void
  212. const Foo = defineVaporAsyncComponent({
  213. loader: () =>
  214. new Promise((_resolve, _reject) => {
  215. resolve = _resolve as any
  216. reject = _reject
  217. }),
  218. errorComponent: (props: { error: Error }) =>
  219. template(props.error.message)(),
  220. })
  221. const toggle = ref(true)
  222. const { mount } = define({
  223. setup() {
  224. return createIf(
  225. () => toggle.value,
  226. () => {
  227. return createComponent(Foo)
  228. },
  229. )
  230. },
  231. }).create()
  232. const root = document.createElement('div')
  233. mount(root)
  234. expect(root.innerHTML).toBe('<!--async component--><!--if-->')
  235. const err = new Error('errored out')
  236. reject!(err)
  237. await timeout()
  238. expect(root.innerHTML).toBe('errored out<!--async component--><!--if-->')
  239. expect(
  240. 'Unhandled error during execution of async component loader',
  241. ).toHaveBeenWarned()
  242. toggle.value = false
  243. await nextTick()
  244. expect(root.innerHTML).toBe('<!--if-->')
  245. // errored out on previous load, toggle and mock success this time
  246. toggle.value = true
  247. await nextTick()
  248. expect(root.innerHTML).toBe('<!--async component--><!--if-->')
  249. // should render this time
  250. resolve!(() => template('resolved')())
  251. await timeout()
  252. expect(root.innerHTML).toBe('resolved<!--async component--><!--if-->')
  253. })
  254. test('error with error + loading components', async () => {
  255. let resolve: (comp: VaporComponent) => void
  256. let reject: (e: Error) => void
  257. const Foo = defineVaporAsyncComponent({
  258. loader: () =>
  259. new Promise((_resolve, _reject) => {
  260. resolve = _resolve as any
  261. reject = _reject
  262. }),
  263. errorComponent: (props: { error: Error }) =>
  264. template(props.error.message)(),
  265. loadingComponent: () => template('loading')(),
  266. delay: 1,
  267. })
  268. const toggle = ref(true)
  269. const { app, mount } = define({
  270. setup() {
  271. return createIf(
  272. () => toggle.value,
  273. () => {
  274. return createComponent(Foo)
  275. },
  276. )
  277. },
  278. }).create()
  279. const handler = (app.config.errorHandler = vi.fn())
  280. const root = document.createElement('div')
  281. mount(root)
  282. // due to the delay, initial mount should be empty
  283. expect(root.innerHTML).toBe('<!--async component--><!--if-->')
  284. // loading show up after delay
  285. await timeout(1)
  286. expect(root.innerHTML).toBe('loading<!--async component--><!--if-->')
  287. const err = new Error('errored out')
  288. reject!(err)
  289. await timeout()
  290. expect(handler).toHaveBeenCalled()
  291. expect(root.innerHTML).toBe('errored out<!--async component--><!--if-->')
  292. toggle.value = false
  293. await nextTick()
  294. expect(root.innerHTML).toBe('<!--if-->')
  295. // errored out on previous load, toggle and mock success this time
  296. toggle.value = true
  297. await nextTick()
  298. expect(root.innerHTML).toBe('<!--async component--><!--if-->')
  299. // loading show up after delay
  300. await timeout(1)
  301. expect(root.innerHTML).toBe('loading<!--async component--><!--if-->')
  302. // should render this time
  303. resolve!(() => template('resolved')())
  304. await timeout()
  305. expect(root.innerHTML).toBe('resolved<!--async component--><!--if-->')
  306. })
  307. test('timeout without error component', async () => {
  308. let resolve: (comp: VaporComponent) => void
  309. const Foo = defineVaporAsyncComponent({
  310. loader: () =>
  311. new Promise(_resolve => {
  312. resolve = _resolve as any
  313. }),
  314. timeout: 1,
  315. })
  316. const { app, mount } = define({
  317. setup() {
  318. return createComponent(Foo)
  319. },
  320. }).create()
  321. const handler = vi.fn()
  322. app.config.errorHandler = handler
  323. const root = document.createElement('div')
  324. mount(root)
  325. expect(root.innerHTML).toBe('<!--async component-->')
  326. await timeout(1)
  327. expect(handler).toHaveBeenCalled()
  328. expect(handler.mock.calls[0][0].message).toMatch(
  329. `Async component timed out after 1ms.`,
  330. )
  331. expect(root.innerHTML).toBe('<!--async component-->')
  332. // if it resolved after timeout, should still work
  333. resolve!(() => template('resolved')())
  334. await timeout()
  335. expect(root.innerHTML).toBe('resolved<!--async component-->')
  336. })
  337. test('timeout with error component', async () => {
  338. let resolve: (comp: VaporComponent) => void
  339. const Foo = defineVaporAsyncComponent({
  340. loader: () =>
  341. new Promise(_resolve => {
  342. resolve = _resolve as any
  343. }),
  344. timeout: 1,
  345. errorComponent: () => template('timed out')(),
  346. })
  347. const root = document.createElement('div')
  348. const { app, mount } = define({
  349. setup() {
  350. return createComponent(Foo)
  351. },
  352. }).create()
  353. const handler = (app.config.errorHandler = vi.fn())
  354. mount(root)
  355. expect(root.innerHTML).toBe('<!--async component-->')
  356. await timeout(1)
  357. expect(handler).toHaveBeenCalled()
  358. expect(root.innerHTML).toBe('timed out<!--async component-->')
  359. // if it resolved after timeout, should still work
  360. resolve!(() => template('resolved')())
  361. await timeout()
  362. expect(root.innerHTML).toBe('resolved<!--async component-->')
  363. })
  364. test('timeout with error + loading components', async () => {
  365. let resolve: (comp: VaporComponent) => void
  366. const Foo = defineVaporAsyncComponent({
  367. loader: () =>
  368. new Promise(_resolve => {
  369. resolve = _resolve as any
  370. }),
  371. delay: 1,
  372. timeout: 16,
  373. errorComponent: () => template('timed out')(),
  374. loadingComponent: () => template('loading')(),
  375. })
  376. const root = document.createElement('div')
  377. const { app, mount } = define({
  378. setup() {
  379. return createComponent(Foo)
  380. },
  381. }).create()
  382. const handler = (app.config.errorHandler = vi.fn())
  383. mount(root)
  384. expect(root.innerHTML).toBe('<!--async component-->')
  385. await timeout(1)
  386. expect(root.innerHTML).toBe('loading<!--async component-->')
  387. await timeout(16)
  388. expect(root.innerHTML).toBe('timed out<!--async component-->')
  389. expect(handler).toHaveBeenCalled()
  390. resolve!(() => template('resolved')())
  391. await timeout()
  392. expect(root.innerHTML).toBe('resolved<!--async component-->')
  393. })
  394. test('timeout without error component, but with loading component', async () => {
  395. let resolve: (comp: VaporComponent) => void
  396. const Foo = defineVaporAsyncComponent({
  397. loader: () =>
  398. new Promise(_resolve => {
  399. resolve = _resolve as any
  400. }),
  401. delay: 1,
  402. timeout: 16,
  403. loadingComponent: () => template('loading')(),
  404. })
  405. const root = document.createElement('div')
  406. const { app, mount } = define({
  407. setup() {
  408. return createComponent(Foo)
  409. },
  410. }).create()
  411. const handler = vi.fn()
  412. app.config.errorHandler = handler
  413. mount(root)
  414. expect(root.innerHTML).toBe('<!--async component-->')
  415. await timeout(1)
  416. expect(root.innerHTML).toBe('loading<!--async component-->')
  417. await timeout(16)
  418. expect(handler).toHaveBeenCalled()
  419. expect(handler.mock.calls[0][0].message).toMatch(
  420. `Async component timed out after 16ms.`,
  421. )
  422. // should still display loading
  423. expect(root.innerHTML).toBe('loading<!--async component-->')
  424. resolve!(() => template('resolved')())
  425. await timeout()
  426. expect(root.innerHTML).toBe('resolved<!--async component-->')
  427. })
  428. test('retry (success)', async () => {
  429. let loaderCallCount = 0
  430. let resolve: (comp: VaporComponent) => void
  431. let reject: (e: Error) => void
  432. const Foo = defineVaporAsyncComponent({
  433. loader: () => {
  434. loaderCallCount++
  435. return new Promise((_resolve, _reject) => {
  436. resolve = _resolve as any
  437. reject = _reject
  438. })
  439. },
  440. onError(error, retry, fail) {
  441. if (error.message.match(/foo/)) {
  442. retry()
  443. } else {
  444. fail()
  445. }
  446. },
  447. })
  448. const root = document.createElement('div')
  449. const { app, mount } = define({
  450. setup() {
  451. return createComponent(Foo)
  452. },
  453. }).create()
  454. const handler = (app.config.errorHandler = vi.fn())
  455. mount(root)
  456. expect(root.innerHTML).toBe('<!--async component-->')
  457. expect(loaderCallCount).toBe(1)
  458. const err = new Error('foo')
  459. reject!(err)
  460. await timeout()
  461. expect(handler).not.toHaveBeenCalled()
  462. expect(loaderCallCount).toBe(2)
  463. expect(root.innerHTML).toBe('<!--async component-->')
  464. // should render this time
  465. resolve!(() => template('resolved')())
  466. await timeout()
  467. expect(handler).not.toHaveBeenCalled()
  468. expect(root.innerHTML).toBe('resolved<!--async component-->')
  469. })
  470. test('retry (skipped)', async () => {
  471. let loaderCallCount = 0
  472. let reject: (e: Error) => void
  473. const Foo = defineVaporAsyncComponent({
  474. loader: () => {
  475. loaderCallCount++
  476. return new Promise((_resolve, _reject) => {
  477. reject = _reject
  478. })
  479. },
  480. onError(error, retry, fail) {
  481. if (error.message.match(/bar/)) {
  482. retry()
  483. } else {
  484. fail()
  485. }
  486. },
  487. })
  488. const root = document.createElement('div')
  489. const { app, mount } = define({
  490. setup() {
  491. return createComponent(Foo)
  492. },
  493. }).create()
  494. const handler = (app.config.errorHandler = vi.fn())
  495. mount(root)
  496. expect(root.innerHTML).toBe('<!--async component-->')
  497. expect(loaderCallCount).toBe(1)
  498. const err = new Error('foo')
  499. reject!(err)
  500. await timeout()
  501. // should fail because retryWhen returns false
  502. expect(handler).toHaveBeenCalled()
  503. expect(handler.mock.calls[0][0]).toBe(err)
  504. expect(loaderCallCount).toBe(1)
  505. expect(root.innerHTML).toBe('<!--async component-->')
  506. })
  507. test('retry (fail w/ max retry attempts)', async () => {
  508. let loaderCallCount = 0
  509. let reject: (e: Error) => void
  510. const Foo = defineVaporAsyncComponent({
  511. loader: () => {
  512. loaderCallCount++
  513. return new Promise((_resolve, _reject) => {
  514. reject = _reject
  515. })
  516. },
  517. onError(error, retry, fail, attempts) {
  518. if (error.message.match(/foo/) && attempts <= 1) {
  519. retry()
  520. } else {
  521. fail()
  522. }
  523. },
  524. })
  525. const root = document.createElement('div')
  526. const { app, mount } = define({
  527. setup() {
  528. return createComponent(Foo)
  529. },
  530. }).create()
  531. const handler = (app.config.errorHandler = vi.fn())
  532. mount(root)
  533. expect(root.innerHTML).toBe('<!--async component-->')
  534. expect(loaderCallCount).toBe(1)
  535. // first retry
  536. const err = new Error('foo')
  537. reject!(err)
  538. await timeout()
  539. expect(handler).not.toHaveBeenCalled()
  540. expect(loaderCallCount).toBe(2)
  541. expect(root.innerHTML).toBe('<!--async component-->')
  542. // 2nd retry, should fail due to reaching maxRetries
  543. reject!(err)
  544. await timeout()
  545. expect(handler).toHaveBeenCalled()
  546. expect(handler.mock.calls[0][0]).toBe(err)
  547. expect(loaderCallCount).toBe(2)
  548. expect(root.innerHTML).toBe('<!--async component-->')
  549. })
  550. test('template ref forwarding', async () => {
  551. let resolve: (comp: VaporComponent) => void
  552. const Foo = defineVaporAsyncComponent(
  553. () =>
  554. new Promise(r => {
  555. resolve = r as any
  556. }),
  557. )
  558. const fooRef = ref<any>(null)
  559. const toggle = ref(true)
  560. const root = document.createElement('div')
  561. const { mount } = define({
  562. setup() {
  563. return { fooRef, toggle }
  564. },
  565. render() {
  566. return createIf(
  567. () => toggle.value,
  568. () => {
  569. const setTemplateRef = createTemplateRefSetter()
  570. const n0 = createComponent(Foo, null, null, true)
  571. setTemplateRef(n0, 'fooRef')
  572. return n0
  573. },
  574. )
  575. },
  576. }).create()
  577. mount(root)
  578. expect(root.innerHTML).toBe('<!--async component--><!--if-->')
  579. expect(fooRef.value).toBe(null)
  580. resolve!({
  581. setup: (props, { expose }) => {
  582. expose({
  583. id: 'foo',
  584. })
  585. return template('resolved')()
  586. },
  587. })
  588. // first time resolve, wait for macro task since there are multiple
  589. // microtasks / .then() calls
  590. await timeout()
  591. expect(root.innerHTML).toBe('resolved<!--async component--><!--if-->')
  592. expect(fooRef.value.id).toBe('foo')
  593. toggle.value = false
  594. await nextTick()
  595. expect(root.innerHTML).toBe('<!--if-->')
  596. expect(fooRef.value).toBe(null)
  597. // already resolved component should update on nextTick
  598. toggle.value = true
  599. await nextTick()
  600. expect(root.innerHTML).toBe('resolved<!--async component--><!--if-->')
  601. expect(fooRef.value.id).toBe('foo')
  602. })
  603. test('the forwarded template ref should always exist when doing multi patching', async () => {
  604. let resolve: (comp: VaporComponent) => void
  605. const Foo = defineVaporAsyncComponent(
  606. () =>
  607. new Promise(r => {
  608. resolve = r as any
  609. }),
  610. )
  611. const fooRef = ref<any>(null)
  612. const toggle = ref(true)
  613. const updater = ref(0)
  614. const root = document.createElement('div')
  615. const { mount } = define({
  616. setup() {
  617. return { fooRef, toggle, updater }
  618. },
  619. render() {
  620. return createIf(
  621. () => toggle.value,
  622. () => {
  623. const setTemplateRef = createTemplateRefSetter()
  624. const n0 = createComponent(Foo, null, null, true)
  625. setTemplateRef(n0, 'fooRef')
  626. const n1 = template(`<span>`)()
  627. renderEffect(() => setElementText(n1, updater.value))
  628. return [n0, n1]
  629. },
  630. )
  631. },
  632. }).create()
  633. mount(root)
  634. expect(root.innerHTML).toBe('<!--async component--><span>0</span><!--if-->')
  635. expect(fooRef.value).toBe(null)
  636. resolve!({
  637. setup: (props, { expose }) => {
  638. expose({
  639. id: 'foo',
  640. })
  641. return template('resolved')()
  642. },
  643. })
  644. await timeout()
  645. expect(root.innerHTML).toBe(
  646. 'resolved<!--async component--><span>0</span><!--if-->',
  647. )
  648. expect(fooRef.value.id).toBe('foo')
  649. updater.value++
  650. await nextTick()
  651. expect(root.innerHTML).toBe(
  652. 'resolved<!--async component--><span>1</span><!--if-->',
  653. )
  654. expect(fooRef.value.id).toBe('foo')
  655. toggle.value = false
  656. await nextTick()
  657. expect(root.innerHTML).toBe('<!--if-->')
  658. expect(fooRef.value).toBe(null)
  659. })
  660. test.todo('with suspense', async () => {})
  661. test.todo('suspensible: false', async () => {})
  662. test.todo('suspense with error handling', async () => {})
  663. test('with KeepAlive', async () => {
  664. const spy = vi.fn()
  665. let resolve: (comp: VaporComponent) => void
  666. const Foo = defineVaporAsyncComponent(
  667. () =>
  668. new Promise(r => {
  669. resolve = r as any
  670. }),
  671. )
  672. const Bar = defineVaporAsyncComponent(() =>
  673. Promise.resolve(
  674. defineVaporComponent({
  675. setup() {
  676. return template('Bar')()
  677. },
  678. }),
  679. ),
  680. )
  681. const toggle = ref(true)
  682. const { html } = define({
  683. setup() {
  684. return createComponent(VaporKeepAlive, null, {
  685. default: withVaporCtx(() =>
  686. createIf(
  687. () => toggle.value,
  688. () => createComponent(Foo),
  689. () => createComponent(Bar),
  690. ),
  691. ),
  692. })
  693. },
  694. }).render()
  695. expect(html()).toBe('<!--async component--><!--if-->')
  696. await nextTick()
  697. resolve!(
  698. defineVaporComponent({
  699. setup() {
  700. onActivated(() => {
  701. spy()
  702. })
  703. return template('Foo')()
  704. },
  705. }),
  706. )
  707. await timeout()
  708. expect(html()).toBe('Foo<!--async component--><!--if-->')
  709. expect(spy).toBeCalledTimes(1)
  710. toggle.value = false
  711. await timeout()
  712. expect(html()).toBe('Bar<!--async component--><!--if-->')
  713. })
  714. test('with KeepAlive + include', async () => {
  715. const spy = vi.fn()
  716. let resolve: (comp: VaporComponent) => void
  717. const Foo = defineVaporAsyncComponent(
  718. () =>
  719. new Promise(r => {
  720. resolve = r as any
  721. }),
  722. )
  723. const { html } = define({
  724. setup() {
  725. return createComponent(
  726. VaporKeepAlive,
  727. { include: () => 'Foo' },
  728. {
  729. default: withVaporCtx(() => createComponent(Foo)),
  730. },
  731. )
  732. },
  733. }).render()
  734. expect(html()).toBe('<!--async component-->')
  735. await nextTick()
  736. resolve!(
  737. defineVaporComponent({
  738. name: 'Foo',
  739. setup() {
  740. onActivated(() => {
  741. spy()
  742. })
  743. return template('Foo')()
  744. },
  745. }),
  746. )
  747. await timeout()
  748. expect(html()).toBe('Foo<!--async component-->')
  749. expect(spy).toBeCalledTimes(1)
  750. })
  751. })