error-handling.spec.js 6.5 KB

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