test: test suspense error handling
This commit is contained in:
parent
b378b17076
commit
3b1d87efbe
@ -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')
|
||||||
|
|
||||||
|
@ -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,28 +986,21 @@ 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)
|
instance,
|
||||||
setupRenderEffect(
|
asyncSetupResult,
|
||||||
instance,
|
parentSuspense,
|
||||||
parentSuspense,
|
isSVG
|
||||||
initialVNode,
|
)
|
||||||
container,
|
}
|
||||||
anchor,
|
})
|
||||||
isSVG
|
|
||||||
)
|
|
||||||
updateHOCHostEl(instance, initialVNode.el as HostNode)
|
|
||||||
if (parentSuspense.deps === 0) {
|
|
||||||
resolveSuspense(parentSuspense)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// give it a placeholder
|
// give it a placeholder
|
||||||
const placeholder = (instance.subTree = createVNode(Empty))
|
const placeholder = (instance.subTree = createVNode(Empty))
|
||||||
processEmptyNode(null, placeholder, container, anchor)
|
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(
|
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(
|
||||||
|
@ -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'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user