apiAsyncComponent.spec.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  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. test('error with error + loading components', async () => {
  173. let resolve: (comp: Component) => void
  174. let reject: (e: Error) => void
  175. const Foo = defineAsyncComponent({
  176. loader: () =>
  177. new Promise((_resolve, _reject) => {
  178. resolve = _resolve as any
  179. reject = _reject
  180. }),
  181. errorComponent: (props: { error: Error }) => props.error.message,
  182. loadingComponent: () => 'loading',
  183. delay: 1
  184. })
  185. const toggle = ref(true)
  186. const root = nodeOps.createElement('div')
  187. const app = createApp({
  188. render: () => (toggle.value ? h(Foo) : null)
  189. })
  190. const handler = (app.config.errorHandler = jest.fn())
  191. app.mount(root)
  192. // due to the delay, initial mount should be empty
  193. expect(serializeInner(root)).toBe('<!---->')
  194. // loading show up after delay
  195. await timeout(1)
  196. expect(serializeInner(root)).toBe('loading')
  197. const err = new Error('errored out')
  198. reject!(err)
  199. await timeout()
  200. expect(handler).toHaveBeenCalled()
  201. expect(serializeInner(root)).toBe('errored out')
  202. toggle.value = false
  203. await nextTick()
  204. expect(serializeInner(root)).toBe('<!---->')
  205. // errored out on previous load, toggle and mock success this time
  206. toggle.value = true
  207. await nextTick()
  208. expect(serializeInner(root)).toBe('<!---->')
  209. // loading show up after delay
  210. await timeout(1)
  211. expect(serializeInner(root)).toBe('loading')
  212. // should render this time
  213. resolve!(() => 'resolved')
  214. await timeout()
  215. expect(serializeInner(root)).toBe('resolved')
  216. })
  217. test('timeout without error component', async () => {
  218. let resolve: (comp: Component) => void
  219. const Foo = defineAsyncComponent({
  220. loader: () =>
  221. new Promise(_resolve => {
  222. resolve = _resolve as any
  223. }),
  224. timeout: 1
  225. })
  226. const root = nodeOps.createElement('div')
  227. const app = createApp({
  228. render: () => h(Foo)
  229. })
  230. const handler = (app.config.errorHandler = jest.fn())
  231. app.mount(root)
  232. expect(serializeInner(root)).toBe('<!---->')
  233. await timeout(1)
  234. expect(handler).toHaveBeenCalled()
  235. expect(handler.mock.calls[0][0].message).toMatch(
  236. `Async component timed out after 1ms.`
  237. )
  238. expect(serializeInner(root)).toBe('<!---->')
  239. // if it resolved after timeout, should still work
  240. resolve!(() => 'resolved')
  241. await timeout()
  242. expect(serializeInner(root)).toBe('resolved')
  243. })
  244. test('timeout with error component', async () => {
  245. let resolve: (comp: Component) => void
  246. const Foo = defineAsyncComponent({
  247. loader: () =>
  248. new Promise(_resolve => {
  249. resolve = _resolve as any
  250. }),
  251. timeout: 1,
  252. errorComponent: () => 'timed out'
  253. })
  254. const root = nodeOps.createElement('div')
  255. const app = createApp({
  256. render: () => h(Foo)
  257. })
  258. const handler = (app.config.errorHandler = jest.fn())
  259. app.mount(root)
  260. expect(serializeInner(root)).toBe('<!---->')
  261. await timeout(1)
  262. expect(handler).toHaveBeenCalled()
  263. expect(serializeInner(root)).toBe('timed out')
  264. // if it resolved after timeout, should still work
  265. resolve!(() => 'resolved')
  266. await timeout()
  267. expect(serializeInner(root)).toBe('resolved')
  268. })
  269. test('timeout with error + loading components', async () => {
  270. let resolve: (comp: Component) => void
  271. const Foo = defineAsyncComponent({
  272. loader: () =>
  273. new Promise(_resolve => {
  274. resolve = _resolve as any
  275. }),
  276. delay: 1,
  277. timeout: 16,
  278. errorComponent: () => 'timed out',
  279. loadingComponent: () => 'loading'
  280. })
  281. const root = nodeOps.createElement('div')
  282. const app = createApp({
  283. render: () => h(Foo)
  284. })
  285. const handler = (app.config.errorHandler = jest.fn())
  286. app.mount(root)
  287. expect(serializeInner(root)).toBe('<!---->')
  288. await timeout(1)
  289. expect(serializeInner(root)).toBe('loading')
  290. await timeout(16)
  291. expect(serializeInner(root)).toBe('timed out')
  292. expect(handler).toHaveBeenCalled()
  293. resolve!(() => 'resolved')
  294. await timeout()
  295. expect(serializeInner(root)).toBe('resolved')
  296. })
  297. test('timeout without error component, but with loading component', async () => {
  298. let resolve: (comp: Component) => void
  299. const Foo = defineAsyncComponent({
  300. loader: () =>
  301. new Promise(_resolve => {
  302. resolve = _resolve as any
  303. }),
  304. delay: 1,
  305. timeout: 16,
  306. loadingComponent: () => 'loading'
  307. })
  308. const root = nodeOps.createElement('div')
  309. const app = createApp({
  310. render: () => h(Foo)
  311. })
  312. const handler = (app.config.errorHandler = jest.fn())
  313. app.mount(root)
  314. expect(serializeInner(root)).toBe('<!---->')
  315. await timeout(1)
  316. expect(serializeInner(root)).toBe('loading')
  317. await timeout(16)
  318. expect(handler).toHaveBeenCalled()
  319. expect(handler.mock.calls[0][0].message).toMatch(
  320. `Async component timed out after 16ms.`
  321. )
  322. // should still display loading
  323. expect(serializeInner(root)).toBe('loading')
  324. resolve!(() => 'resolved')
  325. await timeout()
  326. expect(serializeInner(root)).toBe('resolved')
  327. })
  328. test('with suspense', async () => {
  329. let resolve: (comp: Component) => void
  330. const Foo = defineAsyncComponent(
  331. () =>
  332. new Promise(_resolve => {
  333. resolve = _resolve as any
  334. })
  335. )
  336. const root = nodeOps.createElement('div')
  337. const app = createApp({
  338. render: () =>
  339. h(Suspense, null, {
  340. default: () => [h(Foo), ' & ', h(Foo)],
  341. fallback: () => 'loading'
  342. })
  343. })
  344. app.mount(root)
  345. expect(serializeInner(root)).toBe('loading')
  346. resolve!(() => 'resolved')
  347. await timeout()
  348. expect(serializeInner(root)).toBe('resolved & resolved')
  349. })
  350. test('suspensible: false', async () => {
  351. let resolve: (comp: Component) => void
  352. const Foo = defineAsyncComponent({
  353. loader: () =>
  354. new Promise(_resolve => {
  355. resolve = _resolve as any
  356. }),
  357. suspensible: false
  358. })
  359. const root = nodeOps.createElement('div')
  360. const app = createApp({
  361. render: () =>
  362. h(Suspense, null, {
  363. default: () => [h(Foo), ' & ', h(Foo)],
  364. fallback: () => 'loading'
  365. })
  366. })
  367. app.mount(root)
  368. // should not show suspense fallback
  369. expect(serializeInner(root)).toBe('<!----> & <!---->')
  370. resolve!(() => 'resolved')
  371. await timeout()
  372. expect(serializeInner(root)).toBe('resolved & resolved')
  373. })
  374. test('suspense with error handling', async () => {
  375. let reject: (e: Error) => void
  376. const Foo = defineAsyncComponent(
  377. () =>
  378. new Promise((_resolve, _reject) => {
  379. reject = _reject
  380. })
  381. )
  382. const root = nodeOps.createElement('div')
  383. const app = createApp({
  384. render: () =>
  385. h(Suspense, null, {
  386. default: () => [h(Foo), ' & ', h(Foo)],
  387. fallback: () => 'loading'
  388. })
  389. })
  390. const handler = (app.config.errorHandler = jest.fn())
  391. app.mount(root)
  392. expect(serializeInner(root)).toBe('loading')
  393. reject!(new Error('no'))
  394. await timeout()
  395. expect(handler).toHaveBeenCalled()
  396. expect(serializeInner(root)).toBe('<!----> & <!---->')
  397. })
  398. test('retry (success)', async () => {
  399. let loaderCallCount = 0
  400. let resolve: (comp: Component) => void
  401. let reject: (e: Error) => void
  402. const Foo = defineAsyncComponent({
  403. loader: () => {
  404. loaderCallCount++
  405. return new Promise((_resolve, _reject) => {
  406. resolve = _resolve as any
  407. reject = _reject
  408. })
  409. },
  410. retryWhen: error => error.message.match(/foo/)
  411. })
  412. const root = nodeOps.createElement('div')
  413. const app = createApp({
  414. render: () => h(Foo)
  415. })
  416. const handler = (app.config.errorHandler = jest.fn())
  417. app.mount(root)
  418. expect(serializeInner(root)).toBe('<!---->')
  419. expect(loaderCallCount).toBe(1)
  420. const err = new Error('foo')
  421. reject!(err)
  422. await timeout()
  423. expect(handler).not.toHaveBeenCalled()
  424. expect(loaderCallCount).toBe(2)
  425. expect(serializeInner(root)).toBe('<!---->')
  426. // should render this time
  427. resolve!(() => 'resolved')
  428. await timeout()
  429. expect(handler).not.toHaveBeenCalled()
  430. expect(serializeInner(root)).toBe('resolved')
  431. })
  432. test('retry (skipped)', async () => {
  433. let loaderCallCount = 0
  434. let reject: (e: Error) => void
  435. const Foo = defineAsyncComponent({
  436. loader: () => {
  437. loaderCallCount++
  438. return new Promise((_resolve, _reject) => {
  439. reject = _reject
  440. })
  441. },
  442. retryWhen: error => error.message.match(/bar/)
  443. })
  444. const root = nodeOps.createElement('div')
  445. const app = createApp({
  446. render: () => h(Foo)
  447. })
  448. const handler = (app.config.errorHandler = jest.fn())
  449. app.mount(root)
  450. expect(serializeInner(root)).toBe('<!---->')
  451. expect(loaderCallCount).toBe(1)
  452. const err = new Error('foo')
  453. reject!(err)
  454. await timeout()
  455. // should fail because retryWhen returns false
  456. expect(handler).toHaveBeenCalled()
  457. expect(handler.mock.calls[0][0]).toBe(err)
  458. expect(loaderCallCount).toBe(1)
  459. expect(serializeInner(root)).toBe('<!---->')
  460. })
  461. test('retry (fail w/ maxRetries)', async () => {
  462. let loaderCallCount = 0
  463. let reject: (e: Error) => void
  464. const Foo = defineAsyncComponent({
  465. loader: () => {
  466. loaderCallCount++
  467. return new Promise((_resolve, _reject) => {
  468. reject = _reject
  469. })
  470. },
  471. retryWhen: error => error.message.match(/foo/),
  472. maxRetries: 1
  473. })
  474. const root = nodeOps.createElement('div')
  475. const app = createApp({
  476. render: () => h(Foo)
  477. })
  478. const handler = (app.config.errorHandler = jest.fn())
  479. app.mount(root)
  480. expect(serializeInner(root)).toBe('<!---->')
  481. expect(loaderCallCount).toBe(1)
  482. // first retry
  483. const err = new Error('foo')
  484. reject!(err)
  485. await timeout()
  486. expect(handler).not.toHaveBeenCalled()
  487. expect(loaderCallCount).toBe(2)
  488. expect(serializeInner(root)).toBe('<!---->')
  489. // 2nd retry, should fail due to reaching maxRetries
  490. reject!(err)
  491. await timeout()
  492. expect(handler).toHaveBeenCalled()
  493. expect(handler.mock.calls[0][0]).toBe(err)
  494. expect(loaderCallCount).toBe(2)
  495. expect(serializeInner(root)).toBe('<!---->')
  496. })
  497. })