mirror of https://github.com/vuejs/core.git
test(runtime-vapor): errorHandling (#245)
Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
parent
d14c5d93d2
commit
8941779f9d
|
@ -0,0 +1,565 @@
|
|||
import type { Component } from '../src/component'
|
||||
import { type RefEl, setRef } from '../src/dom/templateRef'
|
||||
import { onErrorCaptured, onMounted } from '../src/apiLifecycle'
|
||||
import { createComponent } from '../src/apiCreateComponent'
|
||||
import { makeRender } from './_utils'
|
||||
import { template } from '../src/dom/template'
|
||||
import { watch, watchEffect } from '../src/apiWatch'
|
||||
import { nextTick } from '../src/scheduler'
|
||||
import { ref } from '@vue/reactivity'
|
||||
|
||||
const define = makeRender()
|
||||
|
||||
describe('error handling', () => {
|
||||
test('propagation', () => {
|
||||
const err = new Error('foo')
|
||||
const fn = vi.fn()
|
||||
|
||||
const Comp: Component = {
|
||||
setup() {
|
||||
onErrorCaptured((err, instance, info) => {
|
||||
fn(err, info, 'root')
|
||||
return false
|
||||
})
|
||||
|
||||
return createComponent(Child)
|
||||
},
|
||||
}
|
||||
|
||||
const Child: Component = {
|
||||
name: 'Child',
|
||||
setup() {
|
||||
onErrorCaptured((err, instance, info) => {
|
||||
fn(err, info, 'child')
|
||||
})
|
||||
return createComponent(GrandChild)
|
||||
},
|
||||
}
|
||||
|
||||
const GrandChild: Component = {
|
||||
setup() {
|
||||
onMounted(() => {
|
||||
throw err
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
define(Comp).render()
|
||||
expect(fn).toHaveBeenCalledTimes(2)
|
||||
expect(fn).toHaveBeenCalledWith(err, 'mounted hook', 'root')
|
||||
expect(fn).toHaveBeenCalledWith(err, 'mounted hook', 'child')
|
||||
})
|
||||
|
||||
test('propagation stoppage', () => {
|
||||
const err = new Error('foo')
|
||||
const fn = vi.fn()
|
||||
|
||||
const Comp = {
|
||||
setup() {
|
||||
onErrorCaptured((err, instance, info) => {
|
||||
fn(err, info, 'root')
|
||||
return false
|
||||
})
|
||||
return createComponent(Child)
|
||||
},
|
||||
}
|
||||
|
||||
const Child = {
|
||||
setup() {
|
||||
onErrorCaptured((err, instance, info) => {
|
||||
fn(err, info, 'child')
|
||||
return false
|
||||
})
|
||||
return createComponent(GrandChild)
|
||||
},
|
||||
}
|
||||
|
||||
const GrandChild = {
|
||||
setup() {
|
||||
onMounted(() => {
|
||||
throw err
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
define(Comp).render()
|
||||
expect(fn).toHaveBeenCalledTimes(1)
|
||||
expect(fn).toHaveBeenCalledWith(err, 'mounted hook', 'child')
|
||||
})
|
||||
|
||||
test('async error handling', async () => {
|
||||
const err = new Error('foo')
|
||||
const fn = vi.fn()
|
||||
|
||||
const Comp = {
|
||||
setup() {
|
||||
onErrorCaptured((err, instance, info) => {
|
||||
fn(err, info)
|
||||
return false
|
||||
})
|
||||
return createComponent(Child)
|
||||
},
|
||||
}
|
||||
|
||||
const Child = {
|
||||
setup() {
|
||||
onMounted(async () => {
|
||||
throw err
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
define(Comp).render()
|
||||
expect(fn).not.toHaveBeenCalled()
|
||||
await new Promise(r => setTimeout(r))
|
||||
expect(fn).toHaveBeenCalledWith(err, 'mounted hook')
|
||||
})
|
||||
|
||||
test('error thrown in onErrorCaptured', () => {
|
||||
const err = new Error('foo')
|
||||
const err2 = new Error('bar')
|
||||
const fn = vi.fn()
|
||||
|
||||
const Comp = {
|
||||
setup() {
|
||||
onErrorCaptured((err, instance, info) => {
|
||||
fn(err, info)
|
||||
return false
|
||||
})
|
||||
return createComponent(Child)
|
||||
},
|
||||
}
|
||||
|
||||
const Child = {
|
||||
setup() {
|
||||
onErrorCaptured(() => {
|
||||
throw err2
|
||||
})
|
||||
return createComponent(GrandChild)
|
||||
},
|
||||
}
|
||||
|
||||
const GrandChild = {
|
||||
setup() {
|
||||
onMounted(() => {
|
||||
throw err
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
define(Comp).render()
|
||||
expect(fn).toHaveBeenCalledTimes(2)
|
||||
expect(fn).toHaveBeenCalledWith(err, 'mounted hook')
|
||||
expect(fn).toHaveBeenCalledWith(err2, 'errorCaptured hook')
|
||||
})
|
||||
|
||||
test('setup function', () => {
|
||||
const err = new Error('foo')
|
||||
const fn = vi.fn()
|
||||
|
||||
const Comp = {
|
||||
setup() {
|
||||
onErrorCaptured((err, instance, info) => {
|
||||
fn(err, info)
|
||||
return false
|
||||
})
|
||||
return createComponent(Child)
|
||||
},
|
||||
}
|
||||
|
||||
const Child = {
|
||||
setup() {
|
||||
throw err
|
||||
},
|
||||
}
|
||||
|
||||
define(Comp).render()
|
||||
expect(fn).toHaveBeenCalledWith(err, 'setup function')
|
||||
})
|
||||
|
||||
test('in render function', () => {
|
||||
const err = new Error('foo')
|
||||
const fn = vi.fn()
|
||||
|
||||
const Comp = {
|
||||
setup() {
|
||||
onErrorCaptured((err, instance, info) => {
|
||||
fn(err, info)
|
||||
return false
|
||||
})
|
||||
return createComponent(Child)
|
||||
},
|
||||
}
|
||||
|
||||
const Child = {
|
||||
render() {
|
||||
throw err
|
||||
},
|
||||
}
|
||||
|
||||
define(Comp).render()
|
||||
expect(fn).toHaveBeenCalledWith(err, 'render function')
|
||||
})
|
||||
|
||||
test('in function ref', () => {
|
||||
const err = new Error('foo')
|
||||
const ref = () => {
|
||||
throw err
|
||||
}
|
||||
const fn = vi.fn()
|
||||
|
||||
const Comp = {
|
||||
setup() {
|
||||
onErrorCaptured((err, instance, info) => {
|
||||
fn(err, info)
|
||||
return false
|
||||
})
|
||||
return createComponent(Child)
|
||||
},
|
||||
}
|
||||
|
||||
const Child = {
|
||||
render() {
|
||||
const el = template('<div>')()
|
||||
setRef(el as RefEl, ref)
|
||||
return el
|
||||
},
|
||||
}
|
||||
|
||||
define(Comp).render()
|
||||
expect(fn).toHaveBeenCalledWith(err, 'ref function')
|
||||
})
|
||||
|
||||
test('in effect', () => {
|
||||
const err = new Error('foo')
|
||||
const fn = vi.fn()
|
||||
|
||||
const Comp = {
|
||||
setup() {
|
||||
onErrorCaptured((err, instance, info) => {
|
||||
fn(err, info)
|
||||
return false
|
||||
})
|
||||
return createComponent(Child)
|
||||
},
|
||||
}
|
||||
|
||||
const Child = {
|
||||
setup() {
|
||||
watchEffect(() => {
|
||||
throw err
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
define(Comp).render()
|
||||
expect(fn).toHaveBeenCalledWith(err, 'watcher callback')
|
||||
})
|
||||
|
||||
test('in watch getter', () => {
|
||||
const err = new Error('foo')
|
||||
const fn = vi.fn()
|
||||
|
||||
const Comp = {
|
||||
setup() {
|
||||
onErrorCaptured((err, instance, info) => {
|
||||
fn(err, info)
|
||||
return false
|
||||
})
|
||||
return createComponent(Child)
|
||||
},
|
||||
}
|
||||
|
||||
const Child = {
|
||||
setup() {
|
||||
watch(
|
||||
() => {
|
||||
throw err
|
||||
},
|
||||
() => {},
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
define(Comp).render()
|
||||
expect(fn).toHaveBeenCalledWith(err, 'watcher getter')
|
||||
})
|
||||
|
||||
test('in watch callback', async () => {
|
||||
const err = new Error('foo')
|
||||
const fn = vi.fn()
|
||||
|
||||
const Comp = {
|
||||
setup() {
|
||||
onErrorCaptured((err, instance, info) => {
|
||||
fn(err, info)
|
||||
return false
|
||||
})
|
||||
return createComponent(Child)
|
||||
},
|
||||
}
|
||||
|
||||
const count = ref(0)
|
||||
const Child = {
|
||||
setup() {
|
||||
watch(
|
||||
() => count.value,
|
||||
() => {
|
||||
throw err
|
||||
},
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
define(Comp).render()
|
||||
|
||||
count.value++
|
||||
await nextTick()
|
||||
expect(fn).toHaveBeenCalledWith(err, 'watcher callback')
|
||||
})
|
||||
|
||||
test('in effect cleanup', async () => {
|
||||
const err = new Error('foo')
|
||||
const count = ref(0)
|
||||
const fn = vi.fn()
|
||||
|
||||
const Comp = {
|
||||
setup() {
|
||||
onErrorCaptured((err, instance, info) => {
|
||||
fn(err, info)
|
||||
return false
|
||||
})
|
||||
return createComponent(Child)
|
||||
},
|
||||
}
|
||||
|
||||
const Child = {
|
||||
setup() {
|
||||
watchEffect(onCleanup => {
|
||||
count.value
|
||||
onCleanup(() => {
|
||||
throw err
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
define(Comp).render()
|
||||
|
||||
count.value++
|
||||
await nextTick()
|
||||
expect(fn).toHaveBeenCalledWith(err, 'watcher cleanup function')
|
||||
})
|
||||
|
||||
test('in component event handler via emit', () => {
|
||||
const err = new Error('foo')
|
||||
const fn = vi.fn()
|
||||
|
||||
const Comp = {
|
||||
setup() {
|
||||
onErrorCaptured((err, instance, info) => {
|
||||
fn(err, info)
|
||||
return false
|
||||
})
|
||||
return createComponent(Child, {
|
||||
onFoo: () => {
|
||||
throw err
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
const Child = {
|
||||
setup(props: any, { emit }: any) {
|
||||
emit('foo')
|
||||
},
|
||||
}
|
||||
|
||||
define(Comp).render()
|
||||
expect(fn).toHaveBeenCalledWith(err, 'setup function')
|
||||
})
|
||||
|
||||
test.todo('in component event handler via emit (async)', async () => {
|
||||
const err = new Error('foo')
|
||||
const fn = vi.fn()
|
||||
|
||||
const Comp = {
|
||||
setup() {
|
||||
onErrorCaptured((err, instance, info) => {
|
||||
fn(err, info)
|
||||
return false
|
||||
})
|
||||
return createComponent(Child, {
|
||||
async onFoo() {
|
||||
throw err
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
const Child = {
|
||||
props: ['onFoo'],
|
||||
setup(props: any, { emit }: any) {
|
||||
emit('foo')
|
||||
},
|
||||
}
|
||||
|
||||
define(Comp).render()
|
||||
await nextTick()
|
||||
expect(fn).toHaveBeenCalledWith(err, 'setup function')
|
||||
})
|
||||
|
||||
test.todo('in component event handler via emit (async + array)', async () => {
|
||||
const err = new Error('foo')
|
||||
const fn = vi.fn()
|
||||
|
||||
const res: Promise<any>[] = []
|
||||
const createAsyncHandler = (p: Promise<any>) => () => {
|
||||
res.push(p)
|
||||
return p
|
||||
}
|
||||
|
||||
const Comp = {
|
||||
setup() {
|
||||
onErrorCaptured((err, instance, info) => {
|
||||
fn(err, info)
|
||||
return false
|
||||
})
|
||||
return createComponent(Child, [
|
||||
{
|
||||
onFoo: () => {
|
||||
createAsyncHandler(Promise.reject(err))
|
||||
createAsyncHandler(Promise.resolve(1))
|
||||
},
|
||||
},
|
||||
])
|
||||
},
|
||||
}
|
||||
|
||||
const Child = {
|
||||
setup(props: any, { emit }: any) {
|
||||
emit('foo')
|
||||
},
|
||||
}
|
||||
|
||||
define(Comp).render()
|
||||
|
||||
try {
|
||||
await Promise.all(res)
|
||||
} catch (e: any) {
|
||||
expect(e).toBe(err)
|
||||
}
|
||||
expect(fn).toHaveBeenCalledWith(err, 'component event handler')
|
||||
})
|
||||
|
||||
it('should warn unhandled', () => {
|
||||
const groupCollapsed = vi.spyOn(console, 'groupCollapsed')
|
||||
groupCollapsed.mockImplementation(() => {})
|
||||
const log = vi.spyOn(console, 'log')
|
||||
log.mockImplementation(() => {})
|
||||
|
||||
const err = new Error('foo')
|
||||
const fn = vi.fn()
|
||||
|
||||
const Comp = {
|
||||
setup() {
|
||||
onErrorCaptured((err, instance, info) => {
|
||||
fn(err, info)
|
||||
})
|
||||
return createComponent(Child)
|
||||
},
|
||||
}
|
||||
|
||||
const Child = {
|
||||
setup() {
|
||||
throw err
|
||||
},
|
||||
}
|
||||
|
||||
let caughtError
|
||||
try {
|
||||
define(Comp).render()
|
||||
} catch (caught) {
|
||||
caughtError = caught
|
||||
}
|
||||
expect(fn).toHaveBeenCalledWith(err, 'setup function')
|
||||
expect(
|
||||
`Unhandled error during execution of setup function`,
|
||||
).toHaveBeenWarned()
|
||||
expect(caughtError).toBe(err)
|
||||
|
||||
groupCollapsed.mockRestore()
|
||||
log.mockRestore()
|
||||
})
|
||||
|
||||
//# 3127
|
||||
test.fails('handle error in watch & watchEffect', async () => {
|
||||
const error1 = new Error('error1')
|
||||
const error2 = new Error('error2')
|
||||
const error3 = new Error('error3')
|
||||
const error4 = new Error('error4')
|
||||
const handler = vi.fn()
|
||||
|
||||
const app = define({
|
||||
setup() {
|
||||
const count = ref(1)
|
||||
watch(
|
||||
count,
|
||||
() => {
|
||||
throw error1
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
watch(
|
||||
count,
|
||||
async () => {
|
||||
throw error2
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
watchEffect(() => {
|
||||
throw error3
|
||||
})
|
||||
watchEffect(async () => {
|
||||
throw error4
|
||||
})
|
||||
},
|
||||
}).create()
|
||||
|
||||
app.app.config.errorHandler = handler
|
||||
app.mount()
|
||||
|
||||
await nextTick()
|
||||
expect(handler).toHaveBeenCalledWith(error1, {}, 'watcher callback')
|
||||
expect(handler).toHaveBeenCalledWith(error2, {}, 'watcher callback')
|
||||
expect(handler).toHaveBeenCalledWith(error3, {}, 'watcher callback')
|
||||
expect(handler).toHaveBeenCalledWith(error4, {}, 'watcher callback')
|
||||
expect(handler).toHaveBeenCalledTimes(4)
|
||||
})
|
||||
|
||||
// #9574
|
||||
test.fails('should pause tracking in error handler', async () => {
|
||||
const error = new Error('error')
|
||||
const x = ref(Math.random())
|
||||
|
||||
const handler = vi.fn(() => {
|
||||
x.value
|
||||
x.value = Math.random()
|
||||
})
|
||||
|
||||
const app = define({
|
||||
setup() {
|
||||
throw error
|
||||
},
|
||||
}).create()
|
||||
|
||||
app.app.config.errorHandler = handler
|
||||
app.mount()
|
||||
|
||||
await nextTick()
|
||||
expect(handler).toHaveBeenCalledWith(error, {}, 'render function')
|
||||
expect(handler).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
// native event handler handling should be tested in respective renderers
|
||||
})
|
Loading…
Reference in New Issue