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('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. })