import Vue from 'vue' import { formatComponentName } from 'core/util/debug' const components = createErrorTestComponents() describe('Error handling', () => { // hooks that prevents the component from rendering, but should not // break parent component ;[ ['render', 'render function'], ['beforeCreate', 'beforeCreate hook'], ['created', 'created hook'], ['beforeMount', 'beforeMount hook'] ].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) }) }) // 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'] ].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) }) }) ;[ ['beforeDestroy', 'beforeDestroy hook'], ['destroyed', 'destroyed 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) }) }) 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) { return e.toString() } } const msg = getErrorMsg.call(vm) expect(msg).toHaveBeenWarned() }).thenWaitFor(next => { assertBothInstancesActive(vm).end(next) }).then(done) }) it('should recover from errors in user watcher callback', done => { const vm = createTestInstance(components.userWatcherCallback) vm.n++ waitForUpdate(() => { expect(`Error in callback for watcher "n"`).toHaveBeenWarned() expect(`Error: userWatcherCallback`).toHaveBeenWarned() }).thenWaitFor(next => { assertBothInstancesActive(vm).end(next) }).then(done) }) it('config.errorHandler should capture errors', done => { const spy = Vue.config.errorHandler = jasmine.createSpy('errorHandler') const vm = createTestInstance(components.render) const args = spy.calls.argsFor(0) expect(args[0].toString()).toContain('Error: render') // error expect(args[1]).toBe(vm.$refs.child) // vm expect(args[2]).toContain('render function') // description assertRootInstanceActive(vm).then(() => { Vue.config.errorHandler = null }).then(done) }) it('properly format component names', () => { const vm = new Vue() expect(formatComponentName(vm)).toBe('') vm.$root = null vm.$options.name = 'hello-there' expect(formatComponentName(vm)).toBe('') vm.$options.name = null vm.$options._componentTag = 'foo-bar-1' expect(formatComponentName(vm)).toBe('') vm.$options._componentTag = null vm.$options.__file = '/foo/bar/baz/SomeThing.vue' expect(formatComponentName(vm)).toBe(` at ${vm.$options.__file}`) expect(formatComponentName(vm, false)).toBe('') vm.$options.__file = 'C:\\foo\\bar\\baz\\windows_file.vue' expect(formatComponentName(vm)).toBe(` at ${vm.$options.__file}`) expect(formatComponentName(vm, false)).toBe('') }) }) function createErrorTestComponents () { const components = {} // 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) } // 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) } }) // user watcher components.userWatcherGetter = { props: ['n'], created () { this.$watch(function () { return this.n + this.a.b.c }, val => { console.log('user watcher fired: ' + val) }) }, render (h) { return h('div', this.n) } } components.userWatcherCallback = { props: ['n'], watch: { n () { throw new Error('userWatcherCallback error') } }, render (h) { return h('div', this.n) } } return components } function createTestInstance (Comp) { return new Vue({ data: { n: 0, ok: true }, render (h) { return h('div', [ 'n:' + this.n + '\n', this.ok ? h(Comp, { ref: 'child', props: { n: this.n }}) : null ]) } }).$mount() } function assertRootInstanceActive (vm, chain) { expect(vm.$el.innerHTML).toContain('n:0\n') vm.n++ return waitForUpdate(() => { expect(vm.$el.innerHTML).toContain('n:1\n') }) } function assertBothInstancesActive (vm) { vm.n = 0 return waitForUpdate(() => { expect(vm.$refs.child.$el.innerHTML).toContain('0') }).thenWaitFor(next => { assertRootInstanceActive(vm).then(() => { expect(vm.$refs.child.$el.innerHTML).toContain('1') }).end(next) }) }