apiDefineAsyncComponent.spec.ts 23 KB

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