apiDefineAsyncComponent.spec.ts 27 KB

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