test: test suspense error handling

This commit is contained in:
Evan You 2019-09-12 01:52:14 -04:00
parent b378b17076
commit 3b1d87efbe
3 changed files with 102 additions and 32 deletions

View File

@ -9,7 +9,8 @@ import {
nextTick, nextTick,
onMounted, onMounted,
watch, watch,
onUnmounted onUnmounted,
onErrorCaptured
} from '@vue/runtime-test' } from '@vue/runtime-test'
describe('renderer: suspense', () => { describe('renderer: suspense', () => {
@ -543,7 +544,39 @@ describe('renderer: suspense', () => {
expect(calls).toEqual([`inner mounted`, `outer mounted`]) 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') test.todo('new async dep after resolve should cause suspense to restart')

View File

@ -49,6 +49,7 @@ import {
createSuspenseBoundary, createSuspenseBoundary,
normalizeSuspenseChildren normalizeSuspenseChildren
} from './suspense' } from './suspense'
import { handleError, ErrorCodes } from './errorHandling'
const prodEffectOptions = { const prodEffectOptions = {
scheduler: queueJob scheduler: queueJob
@ -919,7 +920,7 @@ export function createRenderer<
if (__DEV__) { if (__DEV__) {
pushWarningContext(n2) pushWarningContext(n2)
} }
updateComponentPropsAndSlots(instance, n2) updateComponentPreRender(instance, n2)
if (__DEV__) { if (__DEV__) {
popWarningContext() popWarningContext()
} }
@ -985,26 +986,19 @@ export function createRenderer<
// state again // state again
} }
parentSuspense.deps++ parentSuspense.deps++
instance.asyncDep.then(asyncSetupResult => { instance.asyncDep
// unmounted before resolve .catch(err => {
if (instance.isUnmounted || parentSuspense.isUnmounted) { handleError(err, instance, ErrorCodes.SETUP_FUNCTION)
return })
} .then(asyncSetupResult => {
parentSuspense.deps-- // component may be unmounted before resolve
// retry from this component if (!instance.isUnmounted && !parentSuspense.isUnmounted) {
instance.asyncResolved = true retryAsyncComponent(
handleSetupResult(instance, asyncSetupResult, parentSuspense)
setupRenderEffect(
instance, instance,
asyncSetupResult,
parentSuspense, parentSuspense,
initialVNode,
container,
anchor,
isSVG isSVG
) )
updateHOCHostEl(instance, initialVNode.el as HostNode)
if (parentSuspense.deps === 0) {
resolveSuspense(parentSuspense)
} }
}) })
// give it a placeholder // give it a placeholder
@ -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( function setupRenderEffect(
instance: ComponentInternalInstance, instance: ComponentInternalInstance,
parentSuspense: HostSuspsenseBoundary | null, parentSuspense: HostSuspsenseBoundary | null,
@ -1063,7 +1089,7 @@ export function createRenderer<
} }
if (next !== null) { if (next !== null) {
updateComponentPropsAndSlots(instance, next) updateComponentPreRender(instance, next)
} }
const prevTree = instance.subTree const prevTree = instance.subTree
const nextTree = (instance.subTree = renderComponentRoot(instance)) const nextTree = (instance.subTree = renderComponentRoot(instance))
@ -1107,7 +1133,7 @@ export function createRenderer<
}, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions) }, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
} }
function updateComponentPropsAndSlots( function updateComponentPreRender(
instance: ComponentInternalInstance, instance: ComponentInternalInstance,
nextVNode: HostVNode nextVNode: HostVNode
) { ) {
@ -1679,10 +1705,21 @@ export function createRenderer<
} }
} }
function getNextHostNode(vnode: HostVNode): HostNode | null { function getNextHostNode({
return vnode.component === null component,
? hostNextSibling((vnode.anchor || vnode.el) as HostNode) suspense,
: getNextHostNode(vnode.component.subTree) 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( function setRef(

View File

@ -43,7 +43,7 @@ export const ErrorTypeStrings: Record<number | string, string> = {
[ErrorCodes.APP_ERROR_HANDLER]: 'app errorHandler', [ErrorCodes.APP_ERROR_HANDLER]: 'app errorHandler',
[ErrorCodes.APP_WARN_HANDLER]: 'app warnHandler', [ErrorCodes.APP_WARN_HANDLER]: 'app warnHandler',
[ErrorCodes.SCHEDULER]: [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' 'Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/vue'
} }