apiAsyncComponent.spec.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  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. components: { Foo },
  24. render: () => (toggle.value ? h(Foo) : null)
  25. }).mount(root)
  26. expect(serializeInner(root)).toBe('<!---->')
  27. resolve!(() => 'resolved')
  28. // first time resolve, wait for macro task since there are multiple
  29. // microtasks / .then() calls
  30. await timeout()
  31. expect(serializeInner(root)).toBe('resolved')
  32. toggle.value = false
  33. await nextTick()
  34. expect(serializeInner(root)).toBe('<!---->')
  35. // already resolved component should update on nextTick
  36. toggle.value = true
  37. await nextTick()
  38. expect(serializeInner(root)).toBe('resolved')
  39. })
  40. test('with loading component', async () => {
  41. let resolve: (comp: Component) => void
  42. const Foo = defineAsyncComponent({
  43. loader: () =>
  44. new Promise(r => {
  45. resolve = r as any
  46. }),
  47. loading: () => 'loading',
  48. delay: 1 // defaults to 200
  49. })
  50. const toggle = ref(true)
  51. const root = nodeOps.createElement('div')
  52. createApp({
  53. components: { Foo },
  54. render: () => (toggle.value ? h(Foo) : null)
  55. }).mount(root)
  56. // due to the delay, initial mount should be empty
  57. expect(serializeInner(root)).toBe('<!---->')
  58. // loading show up after delay
  59. await timeout(1)
  60. expect(serializeInner(root)).toBe('loading')
  61. resolve!(() => 'resolved')
  62. await timeout()
  63. expect(serializeInner(root)).toBe('resolved')
  64. toggle.value = false
  65. await nextTick()
  66. expect(serializeInner(root)).toBe('<!---->')
  67. // already resolved component should update on nextTick without loading
  68. // state
  69. toggle.value = true
  70. await nextTick()
  71. expect(serializeInner(root)).toBe('resolved')
  72. })
  73. test('with loading component + explicit delay (0)', async () => {
  74. let resolve: (comp: Component) => void
  75. const Foo = defineAsyncComponent({
  76. loader: () =>
  77. new Promise(r => {
  78. resolve = r as any
  79. }),
  80. loading: () => 'loading',
  81. delay: 0
  82. })
  83. const toggle = ref(true)
  84. const root = nodeOps.createElement('div')
  85. createApp({
  86. components: { Foo },
  87. render: () => (toggle.value ? h(Foo) : null)
  88. }).mount(root)
  89. // with delay: 0, should show loading immediately
  90. expect(serializeInner(root)).toBe('loading')
  91. resolve!(() => 'resolved')
  92. await timeout()
  93. expect(serializeInner(root)).toBe('resolved')
  94. toggle.value = false
  95. await nextTick()
  96. expect(serializeInner(root)).toBe('<!---->')
  97. // already resolved component should update on nextTick without loading
  98. // state
  99. toggle.value = true
  100. await nextTick()
  101. expect(serializeInner(root)).toBe('resolved')
  102. })
  103. test('error without error component', async () => {
  104. let resolve: (comp: Component) => void
  105. let reject: (e: Error) => void
  106. const Foo = defineAsyncComponent(
  107. () =>
  108. new Promise((_resolve, _reject) => {
  109. resolve = _resolve as any
  110. reject = _reject
  111. })
  112. )
  113. const toggle = ref(true)
  114. const root = nodeOps.createElement('div')
  115. const app = createApp({
  116. components: { Foo },
  117. render: () => (toggle.value ? h(Foo) : null)
  118. })
  119. const handler = (app.config.errorHandler = jest.fn())
  120. app.mount(root)
  121. expect(serializeInner(root)).toBe('<!---->')
  122. const err = new Error('foo')
  123. reject!(err)
  124. await timeout()
  125. expect(handler).toHaveBeenCalled()
  126. expect(handler.mock.calls[0][0]).toBe(err)
  127. expect(serializeInner(root)).toBe('<!---->')
  128. toggle.value = false
  129. await nextTick()
  130. expect(serializeInner(root)).toBe('<!---->')
  131. // errored out on previous load, toggle and mock success this time
  132. toggle.value = true
  133. await nextTick()
  134. expect(serializeInner(root)).toBe('<!---->')
  135. // should render this time
  136. resolve!(() => 'resolved')
  137. await timeout()
  138. expect(serializeInner(root)).toBe('resolved')
  139. })
  140. test('error with error component', async () => {
  141. let resolve: (comp: Component) => void
  142. let reject: (e: Error) => void
  143. const Foo = defineAsyncComponent({
  144. loader: () =>
  145. new Promise((_resolve, _reject) => {
  146. resolve = _resolve as any
  147. reject = _reject
  148. }),
  149. error: (props: { error: Error }) => props.error.message
  150. })
  151. const toggle = ref(true)
  152. const root = nodeOps.createElement('div')
  153. const app = createApp({
  154. components: { Foo },
  155. render: () => (toggle.value ? h(Foo) : null)
  156. })
  157. const handler = (app.config.errorHandler = jest.fn())
  158. app.mount(root)
  159. expect(serializeInner(root)).toBe('<!---->')
  160. const err = new Error('errored out')
  161. reject!(err)
  162. await timeout()
  163. expect(handler).toHaveBeenCalled()
  164. expect(serializeInner(root)).toBe('errored out')
  165. toggle.value = false
  166. await nextTick()
  167. expect(serializeInner(root)).toBe('<!---->')
  168. // errored out on previous load, toggle and mock success this time
  169. toggle.value = true
  170. await nextTick()
  171. expect(serializeInner(root)).toBe('<!---->')
  172. // should render this time
  173. resolve!(() => 'resolved')
  174. await timeout()
  175. expect(serializeInner(root)).toBe('resolved')
  176. })
  177. test('error with error + loading components', async () => {
  178. let resolve: (comp: Component) => void
  179. let reject: (e: Error) => void
  180. const Foo = defineAsyncComponent({
  181. loader: () =>
  182. new Promise((_resolve, _reject) => {
  183. resolve = _resolve as any
  184. reject = _reject
  185. }),
  186. error: (props: { error: Error }) => props.error.message,
  187. loading: () => 'loading',
  188. delay: 1
  189. })
  190. const toggle = ref(true)
  191. const root = nodeOps.createElement('div')
  192. const app = createApp({
  193. components: { Foo },
  194. render: () => (toggle.value ? h(Foo) : null)
  195. })
  196. const handler = (app.config.errorHandler = jest.fn())
  197. app.mount(root)
  198. // due to the delay, initial mount should be empty
  199. expect(serializeInner(root)).toBe('<!---->')
  200. // loading show up after delay
  201. await timeout(1)
  202. expect(serializeInner(root)).toBe('loading')
  203. const err = new Error('errored out')
  204. reject!(err)
  205. await timeout()
  206. expect(handler).toHaveBeenCalled()
  207. expect(serializeInner(root)).toBe('errored out')
  208. toggle.value = false
  209. await nextTick()
  210. expect(serializeInner(root)).toBe('<!---->')
  211. // errored out on previous load, toggle and mock success this time
  212. toggle.value = true
  213. await nextTick()
  214. expect(serializeInner(root)).toBe('<!---->')
  215. // loading show up after delay
  216. await timeout(1)
  217. expect(serializeInner(root)).toBe('loading')
  218. // should render this time
  219. resolve!(() => 'resolved')
  220. await timeout()
  221. expect(serializeInner(root)).toBe('resolved')
  222. })
  223. test('timeout without error component', async () => {
  224. let resolve: (comp: Component) => void
  225. const Foo = defineAsyncComponent({
  226. loader: () =>
  227. new Promise(_resolve => {
  228. resolve = _resolve as any
  229. }),
  230. timeout: 1
  231. })
  232. const root = nodeOps.createElement('div')
  233. const app = createApp({
  234. components: { Foo },
  235. render: () => h(Foo)
  236. })
  237. const handler = (app.config.errorHandler = jest.fn())
  238. app.mount(root)
  239. expect(serializeInner(root)).toBe('<!---->')
  240. await timeout(1)
  241. expect(handler).toHaveBeenCalled()
  242. expect(handler.mock.calls[0][0].message).toMatch(
  243. `Async component timed out after 1ms.`
  244. )
  245. expect(serializeInner(root)).toBe('<!---->')
  246. // if it resolved after timeout, should still work
  247. resolve!(() => 'resolved')
  248. await timeout()
  249. expect(serializeInner(root)).toBe('resolved')
  250. })
  251. test('timeout with error component', async () => {
  252. let resolve: (comp: Component) => void
  253. const Foo = defineAsyncComponent({
  254. loader: () =>
  255. new Promise(_resolve => {
  256. resolve = _resolve as any
  257. }),
  258. timeout: 1,
  259. error: () => 'timed out'
  260. })
  261. const root = nodeOps.createElement('div')
  262. const app = createApp({
  263. components: { Foo },
  264. render: () => h(Foo)
  265. })
  266. const handler = (app.config.errorHandler = jest.fn())
  267. app.mount(root)
  268. expect(serializeInner(root)).toBe('<!---->')
  269. await timeout(1)
  270. expect(handler).toHaveBeenCalled()
  271. expect(serializeInner(root)).toBe('timed out')
  272. // if it resolved after timeout, should still work
  273. resolve!(() => 'resolved')
  274. await timeout()
  275. expect(serializeInner(root)).toBe('resolved')
  276. })
  277. test('timeout with error + loading components', async () => {
  278. let resolve: (comp: Component) => void
  279. const Foo = defineAsyncComponent({
  280. loader: () =>
  281. new Promise(_resolve => {
  282. resolve = _resolve as any
  283. }),
  284. delay: 1,
  285. timeout: 16,
  286. error: () => 'timed out',
  287. loading: () => 'loading'
  288. })
  289. const root = nodeOps.createElement('div')
  290. const app = createApp({
  291. components: { Foo },
  292. render: () => h(Foo)
  293. })
  294. const handler = (app.config.errorHandler = jest.fn())
  295. app.mount(root)
  296. expect(serializeInner(root)).toBe('<!---->')
  297. await timeout(1)
  298. expect(serializeInner(root)).toBe('loading')
  299. await timeout(16)
  300. expect(serializeInner(root)).toBe('timed out')
  301. expect(handler).toHaveBeenCalled()
  302. resolve!(() => 'resolved')
  303. await timeout()
  304. expect(serializeInner(root)).toBe('resolved')
  305. })
  306. test('timeout without error component, but with loading component', async () => {
  307. let resolve: (comp: Component) => void
  308. const Foo = defineAsyncComponent({
  309. loader: () =>
  310. new Promise(_resolve => {
  311. resolve = _resolve as any
  312. }),
  313. delay: 1,
  314. timeout: 16,
  315. loading: () => 'loading'
  316. })
  317. const root = nodeOps.createElement('div')
  318. const app = createApp({
  319. components: { Foo },
  320. render: () => h(Foo)
  321. })
  322. const handler = (app.config.errorHandler = jest.fn())
  323. app.mount(root)
  324. expect(serializeInner(root)).toBe('<!---->')
  325. await timeout(1)
  326. expect(serializeInner(root)).toBe('loading')
  327. await timeout(16)
  328. expect(handler).toHaveBeenCalled()
  329. expect(handler.mock.calls[0][0].message).toMatch(
  330. `Async component timed out after 16ms.`
  331. )
  332. // should still display loading
  333. expect(serializeInner(root)).toBe('loading')
  334. resolve!(() => 'resolved')
  335. await timeout()
  336. expect(serializeInner(root)).toBe('resolved')
  337. })
  338. test('with suspense', async () => {
  339. let resolve: (comp: Component) => void
  340. const Foo = defineAsyncComponent(
  341. () =>
  342. new Promise(_resolve => {
  343. resolve = _resolve as any
  344. })
  345. )
  346. const root = nodeOps.createElement('div')
  347. const app = createApp({
  348. components: { Foo },
  349. render: () =>
  350. h(Suspense, null, {
  351. default: () => [h(Foo), ' & ', h(Foo)],
  352. fallback: () => 'loading'
  353. })
  354. })
  355. app.mount(root)
  356. expect(serializeInner(root)).toBe('loading')
  357. resolve!(() => 'resolved')
  358. await timeout()
  359. expect(serializeInner(root)).toBe('resolved & resolved')
  360. })
  361. test('suspensible: false', async () => {
  362. let resolve: (comp: Component) => void
  363. const Foo = defineAsyncComponent({
  364. loader: () =>
  365. new Promise(_resolve => {
  366. resolve = _resolve as any
  367. }),
  368. suspensible: false
  369. })
  370. const root = nodeOps.createElement('div')
  371. const app = createApp({
  372. components: { Foo },
  373. render: () =>
  374. h(Suspense, null, {
  375. default: () => [h(Foo), ' & ', h(Foo)],
  376. fallback: () => 'loading'
  377. })
  378. })
  379. app.mount(root)
  380. // should not show suspense fallback
  381. expect(serializeInner(root)).toBe('<!----> & <!---->')
  382. resolve!(() => 'resolved')
  383. await timeout()
  384. expect(serializeInner(root)).toBe('resolved & resolved')
  385. })
  386. test('suspense with error handling', async () => {
  387. let reject: (e: Error) => void
  388. const Foo = defineAsyncComponent(
  389. () =>
  390. new Promise((_resolve, _reject) => {
  391. reject = _reject
  392. })
  393. )
  394. const root = nodeOps.createElement('div')
  395. const app = createApp({
  396. components: { Foo },
  397. render: () =>
  398. h(Suspense, null, {
  399. default: () => [h(Foo), ' & ', h(Foo)],
  400. fallback: () => 'loading'
  401. })
  402. })
  403. const handler = (app.config.errorHandler = jest.fn())
  404. app.mount(root)
  405. expect(serializeInner(root)).toBe('loading')
  406. reject!(new Error('no'))
  407. await timeout()
  408. expect(handler).toHaveBeenCalled()
  409. expect(serializeInner(root)).toBe('<!----> & <!---->')
  410. })
  411. })