test: test nested suspense & nested async deps
This commit is contained in:
		
							parent
							
								
									bbc3442c52
								
							
						
					
					
						commit
						b30b17d22d
					
				@ -105,6 +105,53 @@ describe('renderer: suspense', () => {
 | 
				
			|||||||
    expect(serializeInner(root)).toBe(`<div>async</div>`)
 | 
					    expect(serializeInner(root)).toBe(`<div>async</div>`)
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  test('nested async deps', async () => {
 | 
				
			||||||
 | 
					    const calls: string[] = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const AsyncOuter = createAsyncComponent({
 | 
				
			||||||
 | 
					      setup() {
 | 
				
			||||||
 | 
					        onMounted(() => {
 | 
				
			||||||
 | 
					          calls.push('outer mounted')
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        return () => h(AsyncInner)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const AsyncInner = createAsyncComponent(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        setup() {
 | 
				
			||||||
 | 
					          onMounted(() => {
 | 
				
			||||||
 | 
					            calls.push('inner mounted')
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					          return () => h('div', 'inner')
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      10
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const Comp = {
 | 
				
			||||||
 | 
					      setup() {
 | 
				
			||||||
 | 
					        return () =>
 | 
				
			||||||
 | 
					          h(Suspense, null, {
 | 
				
			||||||
 | 
					            default: h(AsyncOuter),
 | 
				
			||||||
 | 
					            fallback: h('div', 'fallback')
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const root = nodeOps.createElement('div')
 | 
				
			||||||
 | 
					    render(h(Comp), root)
 | 
				
			||||||
 | 
					    expect(serializeInner(root)).toBe(`<div>fallback</div>`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await deps[0]
 | 
				
			||||||
 | 
					    await nextTick()
 | 
				
			||||||
 | 
					    expect(serializeInner(root)).toBe(`<div>fallback</div>`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await Promise.all(deps)
 | 
				
			||||||
 | 
					    await nextTick()
 | 
				
			||||||
 | 
					    expect(serializeInner(root)).toBe(`<div>inner</div>`)
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  test('onResolve', async () => {
 | 
					  test('onResolve', async () => {
 | 
				
			||||||
    const Async = createAsyncComponent({
 | 
					    const Async = createAsyncComponent({
 | 
				
			||||||
      render() {
 | 
					      render() {
 | 
				
			||||||
@ -286,15 +333,219 @@ describe('renderer: suspense', () => {
 | 
				
			|||||||
    expect(calls).toEqual([])
 | 
					    expect(calls).toEqual([])
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  test('unmount suspense after resolve', () => {})
 | 
					  test('unmount suspense after resolve', async () => {
 | 
				
			||||||
 | 
					    const toggle = ref(true)
 | 
				
			||||||
 | 
					    const unmounted = jest.fn()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  test.todo('unmount suspense before resolve')
 | 
					    const Async = createAsyncComponent({
 | 
				
			||||||
 | 
					      setup() {
 | 
				
			||||||
 | 
					        onUnmounted(unmounted)
 | 
				
			||||||
 | 
					        return () => h('div', 'async')
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  test.todo('nested suspense')
 | 
					    const Comp = {
 | 
				
			||||||
 | 
					      setup() {
 | 
				
			||||||
 | 
					        return () =>
 | 
				
			||||||
 | 
					          toggle.value
 | 
				
			||||||
 | 
					            ? h(Suspense, null, {
 | 
				
			||||||
 | 
					                default: h(Async),
 | 
				
			||||||
 | 
					                fallback: h('div', 'fallback')
 | 
				
			||||||
 | 
					              })
 | 
				
			||||||
 | 
					            : null
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  test.todo('new async dep after resolve should cause suspense to restart')
 | 
					    const root = nodeOps.createElement('div')
 | 
				
			||||||
 | 
					    render(h(Comp), root)
 | 
				
			||||||
 | 
					    expect(serializeInner(root)).toBe(`<div>fallback</div>`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await Promise.all(deps)
 | 
				
			||||||
 | 
					    await nextTick()
 | 
				
			||||||
 | 
					    expect(serializeInner(root)).toBe(`<div>async</div>`)
 | 
				
			||||||
 | 
					    expect(unmounted).not.toHaveBeenCalled()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    toggle.value = false
 | 
				
			||||||
 | 
					    await nextTick()
 | 
				
			||||||
 | 
					    expect(serializeInner(root)).toBe(`<!---->`)
 | 
				
			||||||
 | 
					    expect(unmounted).toHaveBeenCalled()
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  test('unmount suspense before resolve', async () => {
 | 
				
			||||||
 | 
					    const toggle = ref(true)
 | 
				
			||||||
 | 
					    const mounted = jest.fn()
 | 
				
			||||||
 | 
					    const unmounted = jest.fn()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const Async = createAsyncComponent({
 | 
				
			||||||
 | 
					      setup() {
 | 
				
			||||||
 | 
					        onMounted(mounted)
 | 
				
			||||||
 | 
					        onUnmounted(unmounted)
 | 
				
			||||||
 | 
					        return () => h('div', 'async')
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const Comp = {
 | 
				
			||||||
 | 
					      setup() {
 | 
				
			||||||
 | 
					        return () =>
 | 
				
			||||||
 | 
					          toggle.value
 | 
				
			||||||
 | 
					            ? h(Suspense, null, {
 | 
				
			||||||
 | 
					                default: h(Async),
 | 
				
			||||||
 | 
					                fallback: h('div', 'fallback')
 | 
				
			||||||
 | 
					              })
 | 
				
			||||||
 | 
					            : null
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const root = nodeOps.createElement('div')
 | 
				
			||||||
 | 
					    render(h(Comp), root)
 | 
				
			||||||
 | 
					    expect(serializeInner(root)).toBe(`<div>fallback</div>`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    toggle.value = false
 | 
				
			||||||
 | 
					    await nextTick()
 | 
				
			||||||
 | 
					    expect(serializeInner(root)).toBe(`<!---->`)
 | 
				
			||||||
 | 
					    expect(mounted).not.toHaveBeenCalled()
 | 
				
			||||||
 | 
					    expect(unmounted).not.toHaveBeenCalled()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await Promise.all(deps)
 | 
				
			||||||
 | 
					    await nextTick()
 | 
				
			||||||
 | 
					    // should not resolve and cause unmount
 | 
				
			||||||
 | 
					    expect(mounted).not.toHaveBeenCalled()
 | 
				
			||||||
 | 
					    expect(unmounted).not.toHaveBeenCalled()
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  test('nested suspense (parent resolves first)', async () => {
 | 
				
			||||||
 | 
					    const calls: string[] = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const AsyncOuter = createAsyncComponent(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        setup: () => {
 | 
				
			||||||
 | 
					          onMounted(() => {
 | 
				
			||||||
 | 
					            calls.push('outer mounted')
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					          return () => h('div', 'async outer')
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      1
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const AsyncInner = createAsyncComponent(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        setup: () => {
 | 
				
			||||||
 | 
					          onMounted(() => {
 | 
				
			||||||
 | 
					            calls.push('inner mounted')
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					          return () => h('div', 'async inner')
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      10
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const Inner = {
 | 
				
			||||||
 | 
					      setup() {
 | 
				
			||||||
 | 
					        return () =>
 | 
				
			||||||
 | 
					          h(Suspense, null, {
 | 
				
			||||||
 | 
					            default: h(AsyncInner),
 | 
				
			||||||
 | 
					            fallback: h('div', 'fallback inner')
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const Comp = {
 | 
				
			||||||
 | 
					      setup() {
 | 
				
			||||||
 | 
					        return () =>
 | 
				
			||||||
 | 
					          h(Suspense, null, {
 | 
				
			||||||
 | 
					            default: [h(AsyncOuter), h(Inner)],
 | 
				
			||||||
 | 
					            fallback: h('div', 'fallback outer')
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const root = nodeOps.createElement('div')
 | 
				
			||||||
 | 
					    render(h(Comp), root)
 | 
				
			||||||
 | 
					    expect(serializeInner(root)).toBe(`<div>fallback outer</div>`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await deps[0]
 | 
				
			||||||
 | 
					    await nextTick()
 | 
				
			||||||
 | 
					    expect(serializeInner(root)).toBe(
 | 
				
			||||||
 | 
					      `<!----><div>async outer</div><div>fallback inner</div><!---->`
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    expect(calls).toEqual([`outer mounted`])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await Promise.all(deps)
 | 
				
			||||||
 | 
					    await nextTick()
 | 
				
			||||||
 | 
					    expect(serializeInner(root)).toBe(
 | 
				
			||||||
 | 
					      `<!----><div>async outer</div><div>async inner</div><!---->`
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    expect(calls).toEqual([`outer mounted`, `inner mounted`])
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  test('nested suspense (child resolves first)', async () => {
 | 
				
			||||||
 | 
					    const calls: string[] = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const AsyncOuter = createAsyncComponent(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        setup: () => {
 | 
				
			||||||
 | 
					          onMounted(() => {
 | 
				
			||||||
 | 
					            calls.push('outer mounted')
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					          return () => h('div', 'async outer')
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      10
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const AsyncInner = createAsyncComponent(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        setup: () => {
 | 
				
			||||||
 | 
					          onMounted(() => {
 | 
				
			||||||
 | 
					            calls.push('inner mounted')
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					          return () => h('div', 'async inner')
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      1
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const Inner = {
 | 
				
			||||||
 | 
					      setup() {
 | 
				
			||||||
 | 
					        return () =>
 | 
				
			||||||
 | 
					          h(Suspense, null, {
 | 
				
			||||||
 | 
					            default: h(AsyncInner),
 | 
				
			||||||
 | 
					            fallback: h('div', 'fallback inner')
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const Comp = {
 | 
				
			||||||
 | 
					      setup() {
 | 
				
			||||||
 | 
					        return () =>
 | 
				
			||||||
 | 
					          h(Suspense, null, {
 | 
				
			||||||
 | 
					            default: [h(AsyncOuter), h(Inner)],
 | 
				
			||||||
 | 
					            fallback: h('div', 'fallback outer')
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const root = nodeOps.createElement('div')
 | 
				
			||||||
 | 
					    render(h(Comp), root)
 | 
				
			||||||
 | 
					    expect(serializeInner(root)).toBe(`<div>fallback outer</div>`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await deps[1]
 | 
				
			||||||
 | 
					    await nextTick()
 | 
				
			||||||
 | 
					    expect(serializeInner(root)).toBe(`<div>fallback outer</div>`)
 | 
				
			||||||
 | 
					    expect(calls).toEqual([])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await Promise.all(deps)
 | 
				
			||||||
 | 
					    await nextTick()
 | 
				
			||||||
 | 
					    expect(serializeInner(root)).toBe(
 | 
				
			||||||
 | 
					      `<!----><div>async outer</div><div>async inner</div><!---->`
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    expect(calls).toEqual([`inner mounted`, `outer mounted`])
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  test.todo('error handling')
 | 
					  test.todo('error handling')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  test.todo('new async dep after resolve should cause suspense to restart')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  test.todo('portal inside suspense')
 | 
					  test.todo('portal inside suspense')
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
import { VNode, VNodeChild } from './vnode'
 | 
					import { VNode, VNodeChild, isVNode } from './vnode'
 | 
				
			||||||
import { ReactiveEffect, reactive, readonly } from '@vue/reactivity'
 | 
					import { ReactiveEffect, reactive, readonly } from '@vue/reactivity'
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  PublicInstanceProxyHandlers,
 | 
					  PublicInstanceProxyHandlers,
 | 
				
			||||||
@ -279,6 +279,12 @@ export function handleSetupResult(
 | 
				
			|||||||
    // setup returned an inline render function
 | 
					    // setup returned an inline render function
 | 
				
			||||||
    instance.render = setupResult as RenderFunction
 | 
					    instance.render = setupResult as RenderFunction
 | 
				
			||||||
  } else if (isObject(setupResult)) {
 | 
					  } else if (isObject(setupResult)) {
 | 
				
			||||||
 | 
					    if (__DEV__ && isVNode(setupResult)) {
 | 
				
			||||||
 | 
					      warn(
 | 
				
			||||||
 | 
					        `setup() should not return VNodes directly - ` +
 | 
				
			||||||
 | 
					          `return a render function instead.`
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    // setup returned bindings.
 | 
					    // setup returned bindings.
 | 
				
			||||||
    // assuming a render function compiled from template is present.
 | 
					    // assuming a render function compiled from template is present.
 | 
				
			||||||
    instance.renderContext = reactive(setupResult)
 | 
					    instance.renderContext = reactive(setupResult)
 | 
				
			||||||
 | 
				
			|||||||
@ -871,6 +871,7 @@ export function createRenderer<
 | 
				
			|||||||
        hasUnresolvedAncestor = true
 | 
					        hasUnresolvedAncestor = true
 | 
				
			||||||
        break
 | 
					        break
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      parent = parent.parent
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    // no pending parent suspense, flush all jobs
 | 
					    // no pending parent suspense, flush all jobs
 | 
				
			||||||
    if (!hasUnresolvedAncestor) {
 | 
					    if (!hasUnresolvedAncestor) {
 | 
				
			||||||
@ -1509,7 +1510,14 @@ export function createRenderer<
 | 
				
			|||||||
      return
 | 
					      return
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (__FEATURE_SUSPENSE__ && vnode.type === Suspense) {
 | 
					    if (__FEATURE_SUSPENSE__ && vnode.type === Suspense) {
 | 
				
			||||||
      move((vnode.suspense as any).subTree, container, anchor)
 | 
					      const suspense = vnode.suspense as SuspenseBoundary
 | 
				
			||||||
 | 
					      move(
 | 
				
			||||||
 | 
					        suspense.isResolved ? suspense.subTree : suspense.fallbackTree,
 | 
				
			||||||
 | 
					        container,
 | 
				
			||||||
 | 
					        anchor
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      suspense.container = container
 | 
				
			||||||
 | 
					      // suspense.anchor = anchor
 | 
				
			||||||
      return
 | 
					      return
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (vnode.type === Fragment) {
 | 
					    if (vnode.type === Fragment) {
 | 
				
			||||||
 | 
				
			|||||||
@ -124,6 +124,12 @@ export function createBlock(
 | 
				
			|||||||
  return vnode
 | 
					  return vnode
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const knownVNodes = new WeakSet<VNode>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function isVNode(value: any): boolean {
 | 
				
			||||||
 | 
					  return knownVNodes.has(value)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function createVNode(
 | 
					export function createVNode(
 | 
				
			||||||
  type: VNodeTypes,
 | 
					  type: VNodeTypes,
 | 
				
			||||||
  props: { [key: string]: any } | null | 0 = null,
 | 
					  props: { [key: string]: any } | null | 0 = null,
 | 
				
			||||||
@ -198,6 +204,10 @@ export function createVNode(
 | 
				
			|||||||
    trackDynamicNode(vnode)
 | 
					    trackDynamicNode(vnode)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (__DEV__) {
 | 
				
			||||||
 | 
					    knownVNodes.add(vnode)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return vnode
 | 
					  return vnode
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user