]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
test: test nested suspense & nested async deps
authorEvan You <yyx990803@gmail.com>
Thu, 12 Sep 2019 03:44:37 +0000 (23:44 -0400)
committerEvan You <yyx990803@gmail.com>
Thu, 12 Sep 2019 03:44:37 +0000 (23:44 -0400)
packages/runtime-core/__tests__/rendererSuspense.spec.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/createRenderer.ts
packages/runtime-core/src/vnode.ts

index 5e80267b90e48ff005b4818e46f9c76bf710d09b..fa5b6716a8576a9d05de3f722c8779198d1745d4 100644 (file)
@@ -105,6 +105,53 @@ describe('renderer: suspense', () => {
     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 () => {
     const Async = createAsyncComponent({
       render() {
@@ -286,15 +333,219 @@ describe('renderer: suspense', () => {
     expect(calls).toEqual([])
   })
 
-  test('unmount suspense after resolve', () => {})
+  test('unmount suspense after resolve', async () => {
+    const toggle = ref(true)
+    const unmounted = jest.fn()
+
+    const Async = createAsyncComponent({
+      setup() {
+        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>`)
+
+    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()
 
-  test.todo('unmount suspense before resolve')
+    const Async = createAsyncComponent({
+      setup() {
+        onMounted(mounted)
+        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>`)
+
+    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('new async dep after resolve should cause suspense to restart')
+
   test.todo('portal inside suspense')
 })
index 500ac828a9d0efa8ddfffe9edcbe02874160597d..5802549c486f0cd07cbf06cb2c0696f28fd64306 100644 (file)
@@ -1,4 +1,4 @@
-import { VNode, VNodeChild } from './vnode'
+import { VNode, VNodeChild, isVNode } from './vnode'
 import { ReactiveEffect, reactive, readonly } from '@vue/reactivity'
 import {
   PublicInstanceProxyHandlers,
@@ -279,6 +279,12 @@ export function handleSetupResult(
     // setup returned an inline render function
     instance.render = setupResult as RenderFunction
   } else if (isObject(setupResult)) {
+    if (__DEV__ && isVNode(setupResult)) {
+      warn(
+        `setup() should not return VNodes directly - ` +
+          `return a render function instead.`
+      )
+    }
     // setup returned bindings.
     // assuming a render function compiled from template is present.
     instance.renderContext = reactive(setupResult)
index 9d665f4816de379076a91c7ed2d47c056fde34bc..020f7c8fc9b455b2020b59b1c23b453dba94f543 100644 (file)
@@ -871,6 +871,7 @@ export function createRenderer<
         hasUnresolvedAncestor = true
         break
       }
+      parent = parent.parent
     }
     // no pending parent suspense, flush all jobs
     if (!hasUnresolvedAncestor) {
@@ -1509,7 +1510,14 @@ export function createRenderer<
       return
     }
     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
     }
     if (vnode.type === Fragment) {
index 8bcafdd7f98e57859d8e95e6fad9bc53c6dff87d..589042ec3af4280e3f453da8ac4ba08fef8f5390 100644 (file)
@@ -124,6 +124,12 @@ export function createBlock(
   return vnode
 }
 
+const knownVNodes = new WeakSet<VNode>()
+
+export function isVNode(value: any): boolean {
+  return knownVNodes.has(value)
+}
+
 export function createVNode(
   type: VNodeTypes,
   props: { [key: string]: any } | null | 0 = null,
@@ -198,6 +204,10 @@ export function createVNode(
     trackDynamicNode(vnode)
   }
 
+  if (__DEV__) {
+    knownVNodes.add(vnode)
+  }
+
   return vnode
 }