apiAsyncComponent.spec.ts 18 KB

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