import Vue from 'vue' describe('Options errorCaptured', () => { let globalSpy beforeEach(() => { globalSpy = Vue.config.errorHandler = vi.fn() }) afterEach(() => { Vue.config.errorHandler = undefined }) it('should capture error from child component', () => { const spy = vi.fn() let child let err const Child = { created() { child = this err = new Error('child') throw err }, render() {} } new Vue({ errorCaptured: spy, render: h => h(Child) }).$mount() expect(spy).toHaveBeenCalledWith(err, child, 'created hook') // should propagate by default expect(globalSpy).toHaveBeenCalledWith(err, child, 'created hook') }) it('should be able to render the error in itself', done => { let child const Child = { created() { child = this throw new Error('error from child') }, render() {} } const vm = new Vue({ data: { error: null }, errorCaptured(e, vm, info) { expect(vm).toBe(child) this.error = e.toString() + ' in ' + info }, render(h) { if (this.error) { return h('pre', this.error) } return h(Child) } }).$mount() waitForUpdate(() => { expect(vm.$el.textContent).toContain('error from child') expect(vm.$el.textContent).toContain('in created hook') }).then(done) }) it('should not propagate to global handler when returning true', () => { const spy = vi.fn() let child let err const Child = { created() { child = this err = new Error('child') throw err }, render() {} } new Vue({ errorCaptured(err, vm, info) { spy(err, vm, info) return false }, render: h => h(Child, {}) }).$mount() expect(spy).toHaveBeenCalledWith(err, child, 'created hook') // should not propagate expect(globalSpy).not.toHaveBeenCalled() }) it('should propagate to global handler if itself throws error', () => { let child let err const Child = { created() { child = this err = new Error('child') throw err }, render() {} } let err2 const vm = new Vue({ errorCaptured() { err2 = new Error('foo') throw err2 }, render: h => h(Child, {}) }).$mount() expect(globalSpy).toHaveBeenCalledWith(err, child, 'created hook') expect(globalSpy).toHaveBeenCalledWith(err2, vm, 'errorCaptured hook') }) it('should work across multiple parents, mixins and extends', () => { const calls: any[] = [] const Child = { created() { throw new Error('child') }, render() {} } const ErrorBoundaryBase = { errorCaptured() { calls.push(1) } } const mixin = { errorCaptured() { calls.push(2) } } const ErrorBoundaryExtended = { extends: ErrorBoundaryBase, mixins: [mixin], errorCaptured() { calls.push(3) }, render: h => h(Child) } Vue.config.errorHandler = () => { calls.push(5) } new Vue({ errorCaptured() { calls.push(4) }, render: h => h(ErrorBoundaryExtended) }).$mount() expect(calls).toEqual([1, 2, 3, 4, 5]) }) it('should work across multiple parents, mixins and extends with return false', () => { const calls: any[] = [] const Child = { created() { throw new Error('child') }, render() {} } const ErrorBoundaryBase = { errorCaptured() { calls.push(1) } } const mixin = { errorCaptured() { calls.push(2) } } const ErrorBoundaryExtended = { extends: ErrorBoundaryBase, mixins: [mixin], errorCaptured() { calls.push(3) return false }, render: h => h(Child) } Vue.config.errorHandler = () => { calls.push(5) } new Vue({ errorCaptured() { calls.push(4) }, render: h => h(ErrorBoundaryExtended) }).$mount() expect(calls).toEqual([1, 2, 3]) }) // ref: https://github.com/vuejs/vuex/issues/1505 it('should not add watchers to render deps if they are referred from errorCaptured callback', done => { const store = new Vue({ data: { errors: [] } }) const Child = { computed: { test() { throw new Error('render error') } }, render(h) { return h('div', { attrs: { 'data-test': this.test } }) } } new Vue({ errorCaptured(error) { store.errors.push(error) }, render: h => h(Child) }).$mount() // Ensure not to trigger infinite loop waitForUpdate(() => { expect(store.errors.length).toBe(1) expect(store.errors[0]).toEqual(new Error('render error')) }).then(done) }) it('should capture error from watcher', done => { const spy = vi.fn() let child let err const Child = { data() { return { foo: null } }, watch: { foo() { err = new Error('userWatcherCallback error') throw err } }, created() { child = this }, render() {} } new Vue({ errorCaptured: spy, render: h => h(Child) }).$mount() child.foo = 'bar' waitForUpdate(() => { expect(spy).toHaveBeenCalledWith(err, child, 'callback for watcher "foo"') expect(globalSpy).toHaveBeenCalledWith( err, child, 'callback for watcher "foo"' ) }).then(done) }) it('should capture promise error from watcher', done => { const spy = vi.fn() let child let err const Child = { data() { return { foo: null } }, watch: { foo() { err = new Error('userWatcherCallback error') return Promise.reject(err) } }, created() { child = this }, render() {} } new Vue({ errorCaptured: spy, render: h => h(Child) }).$mount() child.foo = 'bar' child.$nextTick(() => { waitForUpdate(() => { expect(spy).toHaveBeenCalledWith( err, child, 'callback for watcher "foo" (Promise/async)' ) expect(globalSpy).toHaveBeenCalledWith( err, child, 'callback for watcher "foo" (Promise/async)' ) }).then(done) }) }) it('should capture error from immediate watcher', done => { const spy = vi.fn() let child let err const Child = { data() { return { foo: 'foo' } }, watch: { foo: { immediate: true, handler() { err = new Error('userImmediateWatcherCallback error') throw err } } }, created() { child = this }, render() {} } new Vue({ errorCaptured: spy, render: h => h(Child) }).$mount() waitForUpdate(() => { expect(spy).toHaveBeenCalledWith( err, child, 'callback for immediate watcher "foo"' ) expect(globalSpy).toHaveBeenCalledWith( err, child, 'callback for immediate watcher "foo"' ) }).then(done) }) it('should capture promise error from immediate watcher', done => { const spy = vi.fn() let child let err const Child = { data() { return { foo: 'foo' } }, watch: { foo: { immediate: true, handler() { err = new Error('userImmediateWatcherCallback error') return Promise.reject(err) } } }, created() { child = this }, render() {} } new Vue({ errorCaptured: spy, render: h => h(Child) }).$mount() waitForUpdate(() => { expect(spy).toHaveBeenCalledWith( err, child, 'callback for immediate watcher "foo" (Promise/async)' ) expect(globalSpy).toHaveBeenCalledWith( err, child, 'callback for immediate watcher "foo" (Promise/async)' ) }).then(done) }) })