import Vue from 'vue' const components = createErrorTestComponents() describe('Error handling', () => { // hooks that prevents the component from rendering, but should not // break parent component ;[ ['data', 'data()'], ['render', 'render'], ['beforeCreate', 'beforeCreate hook'], ['created', 'created hook'], ['beforeMount', 'beforeMount hook'], ['directive bind', 'directive foo bind hook'], ['event', 'event handler for "e"'] ].forEach(([type, description]) => { it(`should recover from errors in ${type}`, done => { const vm = createTestInstance(components[type]) expect(`Error in ${description}`).toHaveBeenWarned() expect(`Error: ${type}`).toHaveBeenWarned() assertRootInstanceActive(vm).then(done) }) }) // hooks that can return rejected promise ;[ ['beforeCreate', 'beforeCreate hook'], ['created', 'created hook'], ['beforeMount', 'beforeMount hook'], ['mounted', 'mounted hook'], ['event', 'event handler for "e"'] ].forEach(([type, description]) => { it(`should recover from promise errors in ${type}`, done => { createTestInstance(components[`${type}Async`]) waitForUpdate(() => { expect(`Error in ${description} (Promise/async)`).toHaveBeenWarned() expect(`Error: ${type}`).toHaveBeenWarned() }).then(done) }) }) // error in mounted hook should affect neither child nor parent it('should recover from errors in mounted hook', done => { const vm = createTestInstance(components.mounted) expect(`Error in mounted hook`).toHaveBeenWarned() expect(`Error: mounted`).toHaveBeenWarned() assertBothInstancesActive(vm).then(done) }) // error in beforeUpdate/updated should affect neither child nor parent ;[ ['beforeUpdate', 'beforeUpdate hook'], ['updated', 'updated hook'], ['directive update', 'directive foo update hook'] ].forEach(([type, description]) => { it(`should recover from errors in ${type} hook`, done => { const vm = createTestInstance(components[type]) assertBothInstancesActive(vm) .then(() => { expect(`Error in ${description}`).toHaveBeenWarned() expect(`Error: ${type}`).toHaveBeenWarned() }) .then(done) }) }) // hooks that can return rejected promise ;[ ['beforeUpdate', 'beforeUpdate hook'], ['updated', 'updated hook'] ].forEach(([type, description]) => { it(`should recover from promise errors in ${type} hook`, done => { const vm = createTestInstance(components[`${type}Async`]) assertBothInstancesActive(vm) .then(() => { expect(`Error in ${description} (Promise/async)`).toHaveBeenWarned() expect(`Error: ${type}`).toHaveBeenWarned() }) .then(done) }) }) ;[ ['beforeDestroy', 'beforeDestroy hook'], ['destroyed', 'destroyed hook'], ['directive unbind', 'directive foo unbind hook'] ].forEach(([type, description]) => { it(`should recover from errors in ${type} hook`, done => { const vm = createTestInstance(components[type]) vm.ok = false waitForUpdate(() => { expect(`Error in ${description}`).toHaveBeenWarned() expect(`Error: ${type}`).toHaveBeenWarned() }) .thenWaitFor(next => { assertRootInstanceActive(vm).end(next) }) .then(done) }) }) ;[ ['beforeDestroy', 'beforeDestroy hook'], ['destroyed', 'destroyed hook'] ].forEach(([type, description]) => { it(`should recover from promise errors in ${type} hook`, done => { const vm = createTestInstance(components[`${type}Async`]) vm.ok = false setTimeout(() => { expect(`Error in ${description} (Promise/async)`).toHaveBeenWarned() expect(`Error: ${type}`).toHaveBeenWarned() assertRootInstanceActive(vm).then(done) }) }) }) it('should recover from errors in user watcher getter', done => { const vm = createTestInstance(components.userWatcherGetter) vm.n++ waitForUpdate(() => { expect(`Error in getter for watcher`).toHaveBeenWarned() function getErrorMsg() { try { this.a.b.c } catch (e: any) { return e.toString() } } const msg = getErrorMsg.call(vm) expect(msg).toHaveBeenWarned() }) .thenWaitFor(next => { assertBothInstancesActive(vm).end(next) }) .then(done) }) ;[ ['userWatcherCallback', 'watcher'], ['userImmediateWatcherCallback', 'immediate watcher'] ].forEach(([type, description]) => { it(`should recover from errors in user ${description} callback`, done => { const vm = createTestInstance(components[type]) assertBothInstancesActive(vm) .then(() => { expect(`Error in callback for ${description} "n"`).toHaveBeenWarned() expect(`Error: ${type} error`).toHaveBeenWarned() }) .then(done) }) it(`should recover from promise errors in user ${description} callback`, done => { const vm = createTestInstance(components[`${type}Async`]) assertBothInstancesActive(vm) .then(() => { expect( `Error in callback for ${description} "n" (Promise/async)` ).toHaveBeenWarned() expect(`Error: ${type} error`).toHaveBeenWarned() }) .then(done) }) }) it('config.errorHandler should capture render errors', done => { const spy = (Vue.config.errorHandler = vi.fn()) const vm = createTestInstance(components.render) const args = spy.mock.calls[0] expect(args[0].toString()).toContain('Error: render') // error expect(args[1]).toBe(vm.$refs.child) // vm expect(args[2]).toContain('render') // description assertRootInstanceActive(vm) .then(() => { Vue.config.errorHandler = undefined }) .then(done) }) it('should capture and recover from nextTick errors', done => { const err1 = new Error('nextTick') const err2 = new Error('nextTick2') const spy = (Vue.config.errorHandler = vi.fn()) Vue.nextTick(() => { throw err1 }) Vue.nextTick(() => { expect(spy).toHaveBeenCalledWith(err1, undefined, 'nextTick') const vm = new Vue() vm.$nextTick(() => { throw err2 }) Vue.nextTick(() => { // should be called with correct instance info expect(spy).toHaveBeenCalledWith(err2, vm, 'nextTick') Vue.config.errorHandler = undefined done() }) }) }) it('should recover from errors thrown in errorHandler itself', () => { Vue.config.errorHandler = () => { throw new Error('error in errorHandler ¯\\_(ツ)_/¯') } const vm = new Vue({ render(h) { throw new Error('error in render') }, renderError(h, err) { return h('div', err.toString()) } }).$mount() expect('error in errorHandler').toHaveBeenWarned() expect('error in render').toHaveBeenWarned() expect(vm.$el.textContent).toContain('error in render') Vue.config.errorHandler = undefined }) // event handlers that can throw errors or return rejected promise ;[ ['single handler', '
'], [ 'multiple handlers', '' ] ].forEach(([type, template]) => { it(`should recover from v-on errors for ${type} registered`, () => { const vm = new Vue({ template, methods: { bork() { throw new Error('v-on') } } }).$mount() document.body.appendChild(vm.$el) global.triggerEvent(vm.$el, 'click') expect('Error in v-on handler').toHaveBeenWarned() expect('Error: v-on').toHaveBeenWarned() document.body.removeChild(vm.$el) }) it(`should recover from v-on async errors for ${type} registered`, done => { const vm = new Vue({ template, methods: { bork() { return new Promise((resolve, reject) => reject(new Error('v-on async')) ) } } }).$mount() document.body.appendChild(vm.$el) global.triggerEvent(vm.$el, 'click') waitForUpdate(() => { expect('Error in v-on handler (Promise/async)').toHaveBeenWarned() expect('Error: v-on').toHaveBeenWarned() document.body.removeChild(vm.$el) }).then(done) }) }) }) function createErrorTestComponents() { const components: any = {} // data components.data = { data() { throw new Error('data') }, render(h) { return h('div') } } // render error components.render = { render(h) { throw new Error('render') } } // lifecycle errors ;['create', 'mount', 'update', 'destroy'].forEach(hook => { // before const before = 'before' + hook.charAt(0).toUpperCase() + hook.slice(1) const beforeComp = (components[before] = { props: ['n'], render(h) { return h('div', this.n) } }) beforeComp[before] = function () { throw new Error(before) } const beforeCompAsync = (components[`${before}Async`] = { props: ['n'], render(h) { return h('div', this.n) } }) beforeCompAsync[before] = function () { return new Promise((resolve, reject) => reject(new Error(before))) } // after const after = hook.replace(/e?$/, 'ed') const afterComp = (components[after] = { props: ['n'], render(h) { return h('div', this.n) } }) afterComp[after] = function () { throw new Error(after) } const afterCompAsync = (components[`${after}Async`] = { props: ['n'], render(h) { return h('div', this.n) } }) afterCompAsync[after] = function () { return new Promise((resolve, reject) => reject(new Error(after))) } }) // directive hooks errors ;['bind', 'update', 'unbind'].forEach(hook => { const key = 'directive ' + hook const dirComp: any = (components[key] = { props: ['n'], template: `