apiDefineAsyncComponent.spec.ts 19 KB

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