]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
test: test suspense error handling
authorEvan You <yyx990803@gmail.com>
Thu, 12 Sep 2019 05:52:14 +0000 (01:52 -0400)
committerEvan You <yyx990803@gmail.com>
Thu, 12 Sep 2019 05:52:14 +0000 (01:52 -0400)
packages/runtime-core/__tests__/rendererSuspense.spec.ts
packages/runtime-core/src/createRenderer.ts
packages/runtime-core/src/errorHandling.ts

index fa5b6716a8576a9d05de3f722c8779198d1745d4..b81d3844b7a52ef4642674cbf8245cc7604d1550 100644 (file)
@@ -9,7 +9,8 @@ import {
   nextTick,
   onMounted,
   watch,
-  onUnmounted
+  onUnmounted,
+  onErrorCaptured
 } from '@vue/runtime-test'
 
 describe('renderer: suspense', () => {
@@ -543,7 +544,39 @@ describe('renderer: suspense', () => {
     expect(calls).toEqual([`inner mounted`, `outer mounted`])
   })
 
-  test.todo('error handling')
+  test('error handling', async () => {
+    const Async = {
+      async setup() {
+        throw new Error('oops')
+      }
+    }
+
+    const Comp = {
+      setup() {
+        const error = ref<any>(null)
+        onErrorCaptured(e => {
+          error.value = e
+          return true
+        })
+
+        return () =>
+          error.value
+            ? h('div', error.value.message)
+            : h(Suspense, null, {
+                default: h(Async),
+                fallback: h('div', 'fallback')
+              })
+      }
+    }
+
+    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>oops</div>`)
+  })
 
   test.todo('new async dep after resolve should cause suspense to restart')
 
index 4a39184769ce5c04983475d3268862845002f1d7..43040e7a87067c23506137f5170eb2351b08b23f 100644 (file)
@@ -49,6 +49,7 @@ import {
   createSuspenseBoundary,
   normalizeSuspenseChildren
 } from './suspense'
+import { handleError, ErrorCodes } from './errorHandling'
 
 const prodEffectOptions = {
   scheduler: queueJob
@@ -919,7 +920,7 @@ export function createRenderer<
           if (__DEV__) {
             pushWarningContext(n2)
           }
-          updateComponentPropsAndSlots(instance, n2)
+          updateComponentPreRender(instance, n2)
           if (__DEV__) {
             popWarningContext()
           }
@@ -985,28 +986,21 @@ export function createRenderer<
         // state again
       }
       parentSuspense.deps++
-      instance.asyncDep.then(asyncSetupResult => {
-        // unmounted before resolve
-        if (instance.isUnmounted || parentSuspense.isUnmounted) {
-          return
-        }
-        parentSuspense.deps--
-        // retry from this component
-        instance.asyncResolved = true
-        handleSetupResult(instance, asyncSetupResult, parentSuspense)
-        setupRenderEffect(
-          instance,
-          parentSuspense,
-          initialVNode,
-          container,
-          anchor,
-          isSVG
-        )
-        updateHOCHostEl(instance, initialVNode.el as HostNode)
-        if (parentSuspense.deps === 0) {
-          resolveSuspense(parentSuspense)
-        }
-      })
+      instance.asyncDep
+        .catch(err => {
+          handleError(err, instance, ErrorCodes.SETUP_FUNCTION)
+        })
+        .then(asyncSetupResult => {
+          // component may be unmounted before resolve
+          if (!instance.isUnmounted && !parentSuspense.isUnmounted) {
+            retryAsyncComponent(
+              instance,
+              asyncSetupResult,
+              parentSuspense,
+              isSVG
+            )
+          }
+        })
       // give it a placeholder
       const placeholder = (instance.subTree = createVNode(Empty))
       processEmptyNode(null, placeholder, container, anchor)
@@ -1028,6 +1022,38 @@ export function createRenderer<
     }
   }
 
+  function retryAsyncComponent(
+    instance: ComponentInternalInstance,
+    asyncSetupResult: unknown,
+    parentSuspense: HostSuspsenseBoundary,
+    isSVG: boolean
+  ) {
+    parentSuspense.deps--
+    // retry from this component
+    instance.asyncResolved = true
+    const { vnode } = instance
+    if (__DEV__) {
+      pushWarningContext(vnode)
+    }
+    handleSetupResult(instance, asyncSetupResult, parentSuspense)
+    setupRenderEffect(
+      instance,
+      parentSuspense,
+      vnode,
+      // component may have been moved before resolve
+      hostParentNode(instance.subTree.el) as HostElement,
+      getNextHostNode(instance.subTree),
+      isSVG
+    )
+    updateHOCHostEl(instance, vnode.el as HostNode)
+    if (__DEV__) {
+      popWarningContext()
+    }
+    if (parentSuspense.deps === 0) {
+      resolveSuspense(parentSuspense)
+    }
+  }
+
   function setupRenderEffect(
     instance: ComponentInternalInstance,
     parentSuspense: HostSuspsenseBoundary | null,
@@ -1063,7 +1089,7 @@ export function createRenderer<
         }
 
         if (next !== null) {
-          updateComponentPropsAndSlots(instance, next)
+          updateComponentPreRender(instance, next)
         }
         const prevTree = instance.subTree
         const nextTree = (instance.subTree = renderComponentRoot(instance))
@@ -1107,7 +1133,7 @@ export function createRenderer<
     }, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
   }
 
-  function updateComponentPropsAndSlots(
+  function updateComponentPreRender(
     instance: ComponentInternalInstance,
     nextVNode: HostVNode
   ) {
@@ -1679,10 +1705,21 @@ export function createRenderer<
     }
   }
 
-  function getNextHostNode(vnode: HostVNode): HostNode | null {
-    return vnode.component === null
-      ? hostNextSibling((vnode.anchor || vnode.el) as HostNode)
-      : getNextHostNode(vnode.component.subTree)
+  function getNextHostNode({
+    component,
+    suspense,
+    anchor,
+    el
+  }: HostVNode): HostNode | null {
+    if (component !== null) {
+      return getNextHostNode(component.subTree)
+    }
+    if (__FEATURE_SUSPENSE__ && suspense !== null) {
+      return getNextHostNode(
+        suspense.isResolved ? suspense.subTree : suspense.fallbackTree
+      )
+    }
+    return hostNextSibling((anchor || el) as HostNode)
   }
 
   function setRef(
index 7a614c8527cc6e2d7a18aa1c17830eb3f7e3d8e6..678b0a3d71df00c030850ab0dd517acaf3d843d1 100644 (file)
@@ -43,7 +43,7 @@ export const ErrorTypeStrings: Record<number | string, string> = {
   [ErrorCodes.APP_ERROR_HANDLER]: 'app errorHandler',
   [ErrorCodes.APP_WARN_HANDLER]: 'app warnHandler',
   [ErrorCodes.SCHEDULER]:
-    'scheduler flush. This may be a Vue internals bug. ' +
+    'scheduler flush. This is likely a Vue internals bug. ' +
     'Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/vue'
 }