rendererSuspense.spec.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. import {
  2. h,
  3. ref,
  4. Suspense,
  5. ComponentOptions,
  6. render,
  7. nodeOps,
  8. serializeInner,
  9. nextTick,
  10. onMounted,
  11. watch,
  12. onUnmounted
  13. } from '@vue/runtime-test'
  14. describe('renderer: suspense', () => {
  15. const deps: Promise<any>[] = []
  16. beforeEach(() => {
  17. deps.length = 0
  18. })
  19. // a simple async factory for testing purposes only.
  20. function createAsyncComponent<T extends ComponentOptions>(
  21. comp: T,
  22. delay: number = 0
  23. ) {
  24. return {
  25. async setup(props: any, { slots }: any) {
  26. const p: Promise<T> = new Promise(r => setTimeout(() => r(comp), delay))
  27. deps.push(p)
  28. const Inner = await p
  29. return () => h(Inner, props, slots)
  30. }
  31. }
  32. }
  33. it('basic usage (nested + multiple deps)', async () => {
  34. const msg = ref('hello')
  35. const AsyncChild = createAsyncComponent({
  36. setup(props: { msg: string }) {
  37. return () => h('div', props.msg)
  38. }
  39. })
  40. const AsyncChild2 = createAsyncComponent(
  41. {
  42. setup(props: { msg: string }) {
  43. return () => h('div', props.msg)
  44. }
  45. },
  46. 10
  47. )
  48. const Mid = {
  49. setup() {
  50. return () =>
  51. h(AsyncChild, {
  52. msg: msg.value
  53. })
  54. }
  55. }
  56. const Comp = {
  57. setup() {
  58. return () =>
  59. h(Suspense, [msg.value, h(Mid), h(AsyncChild2, { msg: 'child 2' })])
  60. }
  61. }
  62. const root = nodeOps.createElement('div')
  63. render(h(Comp), root)
  64. expect(serializeInner(root)).toBe(`<!---->`)
  65. await Promise.all(deps)
  66. await nextTick()
  67. expect(serializeInner(root)).toBe(
  68. `<!---->hello<div>hello</div><div>child 2</div><!---->`
  69. )
  70. })
  71. test('fallback content', async () => {
  72. const Async = createAsyncComponent({
  73. render() {
  74. return h('div', 'async')
  75. }
  76. })
  77. const Comp = {
  78. setup() {
  79. return () =>
  80. h(Suspense, null, {
  81. default: h(Async),
  82. fallback: h('div', 'fallback')
  83. })
  84. }
  85. }
  86. const root = nodeOps.createElement('div')
  87. render(h(Comp), root)
  88. expect(serializeInner(root)).toBe(`<div>fallback</div>`)
  89. await Promise.all(deps)
  90. await nextTick()
  91. expect(serializeInner(root)).toBe(`<div>async</div>`)
  92. })
  93. test('onResolve', async () => {
  94. const Async = createAsyncComponent({
  95. render() {
  96. return h('div', 'async')
  97. }
  98. })
  99. const onResolve = jest.fn()
  100. const Comp = {
  101. setup() {
  102. return () =>
  103. h(
  104. Suspense,
  105. {
  106. onResolve
  107. },
  108. {
  109. default: h(Async),
  110. fallback: h('div', 'fallback')
  111. }
  112. )
  113. }
  114. }
  115. const root = nodeOps.createElement('div')
  116. render(h(Comp), root)
  117. expect(serializeInner(root)).toBe(`<div>fallback</div>`)
  118. expect(onResolve).not.toHaveBeenCalled()
  119. await Promise.all(deps)
  120. await nextTick()
  121. expect(serializeInner(root)).toBe(`<div>async</div>`)
  122. expect(onResolve).toHaveBeenCalled()
  123. })
  124. test('buffer mounted/updated hooks & watch callbacks', async () => {
  125. const deps: Promise<any>[] = []
  126. const calls: string[] = []
  127. const toggle = ref(true)
  128. const Async = {
  129. async setup() {
  130. const p = new Promise(r => setTimeout(r, 1))
  131. deps.push(p)
  132. watch(() => {
  133. calls.push('watch callback')
  134. })
  135. onMounted(() => {
  136. calls.push('mounted')
  137. })
  138. onUnmounted(() => {
  139. calls.push('unmounted')
  140. })
  141. await p
  142. return () => h('div', 'async')
  143. }
  144. }
  145. const Comp = {
  146. setup() {
  147. return () =>
  148. h(Suspense, null, {
  149. default: toggle.value ? h(Async) : null,
  150. fallback: h('div', 'fallback')
  151. })
  152. }
  153. }
  154. const root = nodeOps.createElement('div')
  155. render(h(Comp), root)
  156. expect(serializeInner(root)).toBe(`<div>fallback</div>`)
  157. expect(calls).toEqual([])
  158. await Promise.all(deps)
  159. await nextTick()
  160. expect(serializeInner(root)).toBe(`<div>async</div>`)
  161. expect(calls).toEqual([`watch callback`, `mounted`])
  162. // effects inside an already resolved suspense should happen at normal timing
  163. toggle.value = false
  164. await nextTick()
  165. expect(serializeInner(root)).toBe(`<!---->`)
  166. expect(calls).toEqual([`watch callback`, `mounted`, 'unmounted'])
  167. })
  168. test('content update before suspense resolve', async () => {
  169. const Async = createAsyncComponent({
  170. setup(props: { msg: string }) {
  171. return () => h('div', props.msg)
  172. }
  173. })
  174. const msg = ref('foo')
  175. const Comp = {
  176. setup() {
  177. return () =>
  178. h(Suspense, null, {
  179. default: h(Async, { msg: msg.value }),
  180. fallback: h('div', `fallback ${msg.value}`)
  181. })
  182. }
  183. }
  184. const root = nodeOps.createElement('div')
  185. render(h(Comp), root)
  186. expect(serializeInner(root)).toBe(`<div>fallback foo</div>`)
  187. // value changed before resolve
  188. msg.value = 'bar'
  189. await nextTick()
  190. // fallback content should be updated
  191. expect(serializeInner(root)).toBe(`<div>fallback bar</div>`)
  192. await Promise.all(deps)
  193. await nextTick()
  194. // async component should receive updated props/slots when resolved
  195. expect(serializeInner(root)).toBe(`<div>bar</div>`)
  196. })
  197. // mount/unmount hooks should not even fire
  198. test('unmount before suspense resolve', async () => {
  199. const deps: Promise<any>[] = []
  200. const calls: string[] = []
  201. const toggle = ref(true)
  202. const Async = {
  203. async setup() {
  204. const p = new Promise(r => setTimeout(r, 1))
  205. deps.push(p)
  206. watch(() => {
  207. calls.push('watch callback')
  208. })
  209. onMounted(() => {
  210. calls.push('mounted')
  211. })
  212. onUnmounted(() => {
  213. calls.push('unmounted')
  214. })
  215. await p
  216. return () => h('div', 'async')
  217. }
  218. }
  219. const Comp = {
  220. setup() {
  221. return () =>
  222. h(Suspense, null, {
  223. default: toggle.value ? h(Async) : null,
  224. fallback: h('div', 'fallback')
  225. })
  226. }
  227. }
  228. const root = nodeOps.createElement('div')
  229. render(h(Comp), root)
  230. expect(serializeInner(root)).toBe(`<div>fallback</div>`)
  231. expect(calls).toEqual([])
  232. // remvoe the async dep before it's resolved
  233. toggle.value = false
  234. await nextTick()
  235. // should cause the suspense to resolve immediately
  236. expect(serializeInner(root)).toBe(`<!---->`)
  237. await Promise.all(deps)
  238. await nextTick()
  239. expect(serializeInner(root)).toBe(`<!---->`)
  240. // should discard effects
  241. expect(calls).toEqual([])
  242. })
  243. test.todo('unmount suspense after resolve')
  244. test.todo('unmount suspense before resolve')
  245. test.todo('nested suspense')
  246. test.todo('new async dep after resolve should cause suspense to restart')
  247. test.todo('error handling')
  248. test.todo('portal inside suspense')
  249. })