rendererSuspense.spec.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  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. // test resume for returning bindings
  143. return {
  144. msg: 'async'
  145. }
  146. },
  147. render(this: any) {
  148. return h('div', this.msg)
  149. }
  150. }
  151. const Comp = {
  152. setup() {
  153. return () =>
  154. h(Suspense, null, {
  155. default: toggle.value ? h(Async) : null,
  156. fallback: h('div', 'fallback')
  157. })
  158. }
  159. }
  160. const root = nodeOps.createElement('div')
  161. render(h(Comp), root)
  162. expect(serializeInner(root)).toBe(`<div>fallback</div>`)
  163. expect(calls).toEqual([])
  164. await Promise.all(deps)
  165. await nextTick()
  166. expect(serializeInner(root)).toBe(`<div>async</div>`)
  167. expect(calls).toEqual([`watch callback`, `mounted`])
  168. // effects inside an already resolved suspense should happen at normal timing
  169. toggle.value = false
  170. await nextTick()
  171. expect(serializeInner(root)).toBe(`<!---->`)
  172. expect(calls).toEqual([`watch callback`, `mounted`, 'unmounted'])
  173. })
  174. // should receive updated props/slots when resolved
  175. test.todo('content update before suspense resolve')
  176. // mount/unmount hooks should not even fire
  177. test.todo('unmount before suspense resolve')
  178. test.todo('nested suspense')
  179. test.todo('new async dep after resolve should cause suspense to restart')
  180. test.todo('error handling')
  181. test.todo('portal inside suspense')
  182. })