error-handling.spec.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. import Vue from 'vue'
  2. import { formatComponentName } from 'core/util/debug'
  3. const components = createErrorTestComponents()
  4. describe('Error handling', () => {
  5. // hooks that prevents the component from rendering, but should not
  6. // break parent component
  7. ;[
  8. ['render', 'render function'],
  9. ['beforeCreate', 'beforeCreate hook'],
  10. ['created', 'created hook'],
  11. ['beforeMount', 'beforeMount hook']
  12. ].forEach(([type, description]) => {
  13. it(`should recover from errors in ${type}`, done => {
  14. const vm = createTestInstance(components[type])
  15. expect(`Error in ${description}`).toHaveBeenWarned()
  16. expect(`Error: ${type}`).toHaveBeenWarned()
  17. assertRootInstanceActive(vm).then(done)
  18. })
  19. })
  20. // error in mounted hook should affect neither child nor parent
  21. it('should recover from errors in mounted hook', done => {
  22. const vm = createTestInstance(components.mounted)
  23. expect(`Error in mounted hook`).toHaveBeenWarned()
  24. expect(`Error: mounted`).toHaveBeenWarned()
  25. assertBothInstancesActive(vm).then(done)
  26. })
  27. // error in beforeUpdate/updated should affect neither child nor parent
  28. ;[
  29. ['beforeUpdate', 'beforeUpdate hook'],
  30. ['updated', 'updated hook']
  31. ].forEach(([type, description]) => {
  32. it(`should recover from errors in ${type} hook`, done => {
  33. const vm = createTestInstance(components[type])
  34. assertBothInstancesActive(vm).then(() => {
  35. expect(`Error in ${description}`).toHaveBeenWarned()
  36. expect(`Error: ${type}`).toHaveBeenWarned()
  37. }).then(done)
  38. })
  39. })
  40. ;[
  41. ['beforeDestroy', 'beforeDestroy hook'],
  42. ['destroyed', 'destroyed hook']
  43. ].forEach(([type, description]) => {
  44. it(`should recover from errors in ${type} hook`, done => {
  45. const vm = createTestInstance(components[type])
  46. vm.ok = false
  47. waitForUpdate(() => {
  48. expect(`Error in ${description}`).toHaveBeenWarned()
  49. expect(`Error: ${type}`).toHaveBeenWarned()
  50. }).thenWaitFor(next => {
  51. assertRootInstanceActive(vm).end(next)
  52. }).then(done)
  53. })
  54. })
  55. it('should recover from errors in user watcher getter', done => {
  56. const vm = createTestInstance(components.userWatcherGetter)
  57. vm.n++
  58. waitForUpdate(() => {
  59. expect(`Error in getter for watcher`).toHaveBeenWarned()
  60. function getErrorMsg () {
  61. try {
  62. this.a.b.c
  63. } catch (e) {
  64. return e.toString()
  65. }
  66. }
  67. const msg = getErrorMsg.call(vm)
  68. expect(msg).toHaveBeenWarned()
  69. }).thenWaitFor(next => {
  70. assertBothInstancesActive(vm).end(next)
  71. }).then(done)
  72. })
  73. it('should recover from errors in user watcher callback', done => {
  74. const vm = createTestInstance(components.userWatcherCallback)
  75. vm.n++
  76. waitForUpdate(() => {
  77. expect(`Error in callback for watcher "n"`).toHaveBeenWarned()
  78. expect(`Error: userWatcherCallback`).toHaveBeenWarned()
  79. }).thenWaitFor(next => {
  80. assertBothInstancesActive(vm).end(next)
  81. }).then(done)
  82. })
  83. it('config.errorHandler should capture errors', done => {
  84. const spy = Vue.config.errorHandler = jasmine.createSpy('errorHandler')
  85. const vm = createTestInstance(components.render)
  86. const args = spy.calls.argsFor(0)
  87. expect(args[0].toString()).toContain('Error: render') // error
  88. expect(args[1]).toBe(vm.$refs.child) // vm
  89. expect(args[2]).toContain('render function') // description
  90. assertRootInstanceActive(vm).then(() => {
  91. Vue.config.errorHandler = null
  92. }).then(done)
  93. })
  94. it('properly format component names', () => {
  95. const vm = new Vue()
  96. expect(formatComponentName(vm)).toBe('<Root>')
  97. vm.$root = null
  98. vm.$options.name = 'hello-there'
  99. expect(formatComponentName(vm)).toBe('<HelloThere>')
  100. vm.$options.name = null
  101. vm.$options._componentTag = 'foo-bar-1'
  102. expect(formatComponentName(vm)).toBe('<FooBar1>')
  103. vm.$options._componentTag = null
  104. vm.$options.__file = '/foo/bar/baz/SomeThing.vue'
  105. expect(formatComponentName(vm)).toBe(`<SomeThing> at ${vm.$options.__file}`)
  106. expect(formatComponentName(vm, false)).toBe('<SomeThing>')
  107. vm.$options.__file = 'C:\\foo\\bar\\baz\\windows_file.vue'
  108. expect(formatComponentName(vm)).toBe(`<WindowsFile> at ${vm.$options.__file}`)
  109. expect(formatComponentName(vm, false)).toBe('<WindowsFile>')
  110. })
  111. })
  112. function createErrorTestComponents () {
  113. const components = {}
  114. // render error
  115. components.render = {
  116. render (h) {
  117. throw new Error('render')
  118. }
  119. }
  120. // lifecycle errors
  121. ;['create', 'mount', 'update', 'destroy'].forEach(hook => {
  122. // before
  123. const before = 'before' + hook.charAt(0).toUpperCase() + hook.slice(1)
  124. const beforeComp = components[before] = {
  125. props: ['n'],
  126. render (h) {
  127. return h('div', this.n)
  128. }
  129. }
  130. beforeComp[before] = function () {
  131. throw new Error(before)
  132. }
  133. // after
  134. const after = hook.replace(/e?$/, 'ed')
  135. const afterComp = components[after] = {
  136. props: ['n'],
  137. render (h) {
  138. return h('div', this.n)
  139. }
  140. }
  141. afterComp[after] = function () {
  142. throw new Error(after)
  143. }
  144. })
  145. // user watcher
  146. components.userWatcherGetter = {
  147. props: ['n'],
  148. created () {
  149. this.$watch(function () {
  150. return this.n + this.a.b.c
  151. }, val => {
  152. console.log('user watcher fired: ' + val)
  153. })
  154. },
  155. render (h) {
  156. return h('div', this.n)
  157. }
  158. }
  159. components.userWatcherCallback = {
  160. props: ['n'],
  161. watch: {
  162. n () {
  163. throw new Error('userWatcherCallback error')
  164. }
  165. },
  166. render (h) {
  167. return h('div', this.n)
  168. }
  169. }
  170. return components
  171. }
  172. function createTestInstance (Comp) {
  173. return new Vue({
  174. data: {
  175. n: 0,
  176. ok: true
  177. },
  178. render (h) {
  179. return h('div', [
  180. 'n:' + this.n + '\n',
  181. this.ok
  182. ? h(Comp, { ref: 'child', props: { n: this.n }})
  183. : null
  184. ])
  185. }
  186. }).$mount()
  187. }
  188. function assertRootInstanceActive (vm, chain) {
  189. expect(vm.$el.innerHTML).toContain('n:0\n')
  190. vm.n++
  191. return waitForUpdate(() => {
  192. expect(vm.$el.innerHTML).toContain('n:1\n')
  193. })
  194. }
  195. function assertBothInstancesActive (vm) {
  196. vm.n = 0
  197. return waitForUpdate(() => {
  198. expect(vm.$refs.child.$el.innerHTML).toContain('0')
  199. }).thenWaitFor(next => {
  200. assertRootInstanceActive(vm).then(() => {
  201. expect(vm.$refs.child.$el.innerHTML).toContain('1')
  202. }).end(next)
  203. })
  204. }