apiAsyncComponent.spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. import {
  2. createAsyncComponent,
  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: createAsyncComponent', () => {
  12. test('simple usage', async () => {
  13. let resolve: (comp: Component) => void
  14. const Foo = createAsyncComponent(
  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 = createAsyncComponent({
  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 = createAsyncComponent({
  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 = createAsyncComponent(
  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 = createAsyncComponent({
  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. // error handler will not be called if error component is present
  164. expect(handler).not.toHaveBeenCalled()
  165. expect(serializeInner(root)).toBe('errored out')
  166. toggle.value = false
  167. await nextTick()
  168. expect(serializeInner(root)).toBe('<!---->')
  169. // errored out on previous load, toggle and mock success this time
  170. toggle.value = true
  171. await nextTick()
  172. expect(serializeInner(root)).toBe('<!---->')
  173. // should render this time
  174. resolve!(() => 'resolved')
  175. await timeout()
  176. expect(serializeInner(root)).toBe('resolved')
  177. })
  178. test('error with error + loading components', async () => {
  179. let resolve: (comp: Component) => void
  180. let reject: (e: Error) => void
  181. const Foo = createAsyncComponent({
  182. loader: () =>
  183. new Promise((_resolve, _reject) => {
  184. resolve = _resolve as any
  185. reject = _reject
  186. }),
  187. error: (props: { error: Error }) => props.error.message,
  188. loading: () => 'loading',
  189. delay: 1
  190. })
  191. const toggle = ref(true)
  192. const root = nodeOps.createElement('div')
  193. const app = createApp({
  194. components: { Foo },
  195. render: () => (toggle.value ? h(Foo) : null)
  196. })
  197. const handler = (app.config.errorHandler = jest.fn())
  198. app.mount(root)
  199. // due to the delay, initial mount should be empty
  200. expect(serializeInner(root)).toBe('<!---->')
  201. // loading show up after delay
  202. await timeout(1)
  203. expect(serializeInner(root)).toBe('loading')
  204. const err = new Error('errored out')
  205. reject!(err)
  206. await timeout()
  207. // error handler will not be called if error component is present
  208. expect(handler).not.toHaveBeenCalled()
  209. expect(serializeInner(root)).toBe('errored out')
  210. toggle.value = false
  211. await nextTick()
  212. expect(serializeInner(root)).toBe('<!---->')
  213. // errored out on previous load, toggle and mock success this time
  214. toggle.value = true
  215. await nextTick()
  216. expect(serializeInner(root)).toBe('<!---->')
  217. // loading show up after delay
  218. await timeout(1)
  219. expect(serializeInner(root)).toBe('loading')
  220. // should render this time
  221. resolve!(() => 'resolved')
  222. await timeout()
  223. expect(serializeInner(root)).toBe('resolved')
  224. })
  225. test('timeout without error component', async () => {
  226. let resolve: (comp: Component) => void
  227. const Foo = createAsyncComponent({
  228. loader: () =>
  229. new Promise(_resolve => {
  230. resolve = _resolve as any
  231. }),
  232. timeout: 1
  233. })
  234. const root = nodeOps.createElement('div')
  235. const app = createApp({
  236. components: { Foo },
  237. render: () => h(Foo)
  238. })
  239. const handler = (app.config.errorHandler = jest.fn())
  240. app.mount(root)
  241. expect(serializeInner(root)).toBe('<!---->')
  242. await timeout(1)
  243. expect(handler).toHaveBeenCalled()
  244. expect(handler.mock.calls[0][0].message).toMatch(
  245. `Async component timed out after 1ms.`
  246. )
  247. expect(serializeInner(root)).toBe('<!---->')
  248. // if it resolved after timeout, should still work
  249. resolve!(() => 'resolved')
  250. await timeout()
  251. expect(serializeInner(root)).toBe('resolved')
  252. })
  253. test('timeout with error component', async () => {
  254. let resolve: (comp: Component) => void
  255. const Foo = createAsyncComponent({
  256. loader: () =>
  257. new Promise(_resolve => {
  258. resolve = _resolve as any
  259. }),
  260. timeout: 1,
  261. error: () => 'timed out'
  262. })
  263. const root = nodeOps.createElement('div')
  264. const app = createApp({
  265. components: { Foo },
  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).not.toHaveBeenCalled()
  273. expect(serializeInner(root)).toBe('timed out')
  274. // if it resolved after timeout, should still work
  275. resolve!(() => 'resolved')
  276. await timeout()
  277. expect(serializeInner(root)).toBe('resolved')
  278. })
  279. test('timeout with error + loading components', async () => {
  280. let resolve: (comp: Component) => void
  281. const Foo = createAsyncComponent({
  282. loader: () =>
  283. new Promise(_resolve => {
  284. resolve = _resolve as any
  285. }),
  286. delay: 1,
  287. timeout: 16,
  288. error: () => 'timed out',
  289. loading: () => 'loading'
  290. })
  291. const root = nodeOps.createElement('div')
  292. const app = createApp({
  293. components: { Foo },
  294. render: () => h(Foo)
  295. })
  296. app.mount(root)
  297. expect(serializeInner(root)).toBe('<!---->')
  298. await timeout(1)
  299. expect(serializeInner(root)).toBe('loading')
  300. await timeout(16)
  301. expect(serializeInner(root)).toBe('timed out')
  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 = createAsyncComponent({
  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 = createAsyncComponent(
  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 = createAsyncComponent({
  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. // TODO
  387. test.todo('suspense with error handling')
  388. })