errorCaptured.spec.ts 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. import Vue from 'vue'
  2. describe('Options errorCaptured', () => {
  3. let globalSpy
  4. beforeEach(() => {
  5. globalSpy = Vue.config.errorHandler = vi.fn()
  6. })
  7. afterEach(() => {
  8. Vue.config.errorHandler = undefined
  9. })
  10. it('should capture error from child component', () => {
  11. const spy = vi.fn()
  12. let child
  13. let err
  14. const Child = {
  15. created() {
  16. child = this
  17. err = new Error('child')
  18. throw err
  19. },
  20. render() {}
  21. }
  22. new Vue({
  23. errorCaptured: spy,
  24. render: h => h(Child)
  25. }).$mount()
  26. expect(spy).toHaveBeenCalledWith(err, child, 'created hook')
  27. // should propagate by default
  28. expect(globalSpy).toHaveBeenCalledWith(err, child, 'created hook')
  29. })
  30. it('should be able to render the error in itself', done => {
  31. let child
  32. const Child = {
  33. created() {
  34. child = this
  35. throw new Error('error from child')
  36. },
  37. render() {}
  38. }
  39. const vm = new Vue({
  40. data: {
  41. error: null
  42. },
  43. errorCaptured(e, vm, info) {
  44. expect(vm).toBe(child)
  45. this.error = e.toString() + ' in ' + info
  46. },
  47. render(h) {
  48. if (this.error) {
  49. return h('pre', this.error)
  50. }
  51. return h(Child)
  52. }
  53. }).$mount()
  54. waitForUpdate(() => {
  55. expect(vm.$el.textContent).toContain('error from child')
  56. expect(vm.$el.textContent).toContain('in created hook')
  57. }).then(done)
  58. })
  59. it('should not propagate to global handler when returning true', () => {
  60. const spy = vi.fn()
  61. let child
  62. let err
  63. const Child = {
  64. created() {
  65. child = this
  66. err = new Error('child')
  67. throw err
  68. },
  69. render() {}
  70. }
  71. new Vue({
  72. errorCaptured(err, vm, info) {
  73. spy(err, vm, info)
  74. return false
  75. },
  76. render: h => h(Child, {})
  77. }).$mount()
  78. expect(spy).toHaveBeenCalledWith(err, child, 'created hook')
  79. // should not propagate
  80. expect(globalSpy).not.toHaveBeenCalled()
  81. })
  82. it('should propagate to global handler if itself throws error', () => {
  83. let child
  84. let err
  85. const Child = {
  86. created() {
  87. child = this
  88. err = new Error('child')
  89. throw err
  90. },
  91. render() {}
  92. }
  93. let err2
  94. const vm = new Vue({
  95. errorCaptured() {
  96. err2 = new Error('foo')
  97. throw err2
  98. },
  99. render: h => h(Child, {})
  100. }).$mount()
  101. expect(globalSpy).toHaveBeenCalledWith(err, child, 'created hook')
  102. expect(globalSpy).toHaveBeenCalledWith(err2, vm, 'errorCaptured hook')
  103. })
  104. it('should work across multiple parents, mixins and extends', () => {
  105. const calls: any[] = []
  106. const Child = {
  107. created() {
  108. throw new Error('child')
  109. },
  110. render() {}
  111. }
  112. const ErrorBoundaryBase = {
  113. errorCaptured() {
  114. calls.push(1)
  115. }
  116. }
  117. const mixin = {
  118. errorCaptured() {
  119. calls.push(2)
  120. }
  121. }
  122. const ErrorBoundaryExtended = {
  123. extends: ErrorBoundaryBase,
  124. mixins: [mixin],
  125. errorCaptured() {
  126. calls.push(3)
  127. },
  128. render: h => h(Child)
  129. }
  130. Vue.config.errorHandler = () => {
  131. calls.push(5)
  132. }
  133. new Vue({
  134. errorCaptured() {
  135. calls.push(4)
  136. },
  137. render: h => h(ErrorBoundaryExtended)
  138. }).$mount()
  139. expect(calls).toEqual([1, 2, 3, 4, 5])
  140. })
  141. it('should work across multiple parents, mixins and extends with return false', () => {
  142. const calls: any[] = []
  143. const Child = {
  144. created() {
  145. throw new Error('child')
  146. },
  147. render() {}
  148. }
  149. const ErrorBoundaryBase = {
  150. errorCaptured() {
  151. calls.push(1)
  152. }
  153. }
  154. const mixin = {
  155. errorCaptured() {
  156. calls.push(2)
  157. }
  158. }
  159. const ErrorBoundaryExtended = {
  160. extends: ErrorBoundaryBase,
  161. mixins: [mixin],
  162. errorCaptured() {
  163. calls.push(3)
  164. return false
  165. },
  166. render: h => h(Child)
  167. }
  168. Vue.config.errorHandler = () => {
  169. calls.push(5)
  170. }
  171. new Vue({
  172. errorCaptured() {
  173. calls.push(4)
  174. },
  175. render: h => h(ErrorBoundaryExtended)
  176. }).$mount()
  177. expect(calls).toEqual([1, 2, 3])
  178. })
  179. // ref: https://github.com/vuejs/vuex/issues/1505
  180. it('should not add watchers to render deps if they are referred from errorCaptured callback', done => {
  181. const store = new Vue({
  182. data: {
  183. errors: []
  184. }
  185. })
  186. const Child = {
  187. computed: {
  188. test() {
  189. throw new Error('render error')
  190. }
  191. },
  192. render(h) {
  193. return h('div', {
  194. attrs: {
  195. 'data-test': this.test
  196. }
  197. })
  198. }
  199. }
  200. new Vue({
  201. errorCaptured(error) {
  202. store.errors.push(error)
  203. },
  204. render: h => h(Child)
  205. }).$mount()
  206. // Ensure not to trigger infinite loop
  207. waitForUpdate(() => {
  208. expect(store.errors.length).toBe(1)
  209. expect(store.errors[0]).toEqual(new Error('render error'))
  210. }).then(done)
  211. })
  212. it('should capture error from watcher', done => {
  213. const spy = vi.fn()
  214. let child
  215. let err
  216. const Child = {
  217. data() {
  218. return {
  219. foo: null
  220. }
  221. },
  222. watch: {
  223. foo() {
  224. err = new Error('userWatcherCallback error')
  225. throw err
  226. }
  227. },
  228. created() {
  229. child = this
  230. },
  231. render() {}
  232. }
  233. new Vue({
  234. errorCaptured: spy,
  235. render: h => h(Child)
  236. }).$mount()
  237. child.foo = 'bar'
  238. waitForUpdate(() => {
  239. expect(spy).toHaveBeenCalledWith(err, child, 'callback for watcher "foo"')
  240. expect(globalSpy).toHaveBeenCalledWith(
  241. err,
  242. child,
  243. 'callback for watcher "foo"'
  244. )
  245. }).then(done)
  246. })
  247. it('should capture promise error from watcher', done => {
  248. const spy = vi.fn()
  249. let child
  250. let err
  251. const Child = {
  252. data() {
  253. return {
  254. foo: null
  255. }
  256. },
  257. watch: {
  258. foo() {
  259. err = new Error('userWatcherCallback error')
  260. return Promise.reject(err)
  261. }
  262. },
  263. created() {
  264. child = this
  265. },
  266. render() {}
  267. }
  268. new Vue({
  269. errorCaptured: spy,
  270. render: h => h(Child)
  271. }).$mount()
  272. child.foo = 'bar'
  273. child.$nextTick(() => {
  274. waitForUpdate(() => {
  275. expect(spy).toHaveBeenCalledWith(
  276. err,
  277. child,
  278. 'callback for watcher "foo" (Promise/async)'
  279. )
  280. expect(globalSpy).toHaveBeenCalledWith(
  281. err,
  282. child,
  283. 'callback for watcher "foo" (Promise/async)'
  284. )
  285. }).then(done)
  286. })
  287. })
  288. it('should capture error from immediate watcher', done => {
  289. const spy = vi.fn()
  290. let child
  291. let err
  292. const Child = {
  293. data() {
  294. return {
  295. foo: 'foo'
  296. }
  297. },
  298. watch: {
  299. foo: {
  300. immediate: true,
  301. handler() {
  302. err = new Error('userImmediateWatcherCallback error')
  303. throw err
  304. }
  305. }
  306. },
  307. created() {
  308. child = this
  309. },
  310. render() {}
  311. }
  312. new Vue({
  313. errorCaptured: spy,
  314. render: h => h(Child)
  315. }).$mount()
  316. waitForUpdate(() => {
  317. expect(spy).toHaveBeenCalledWith(
  318. err,
  319. child,
  320. 'callback for immediate watcher "foo"'
  321. )
  322. expect(globalSpy).toHaveBeenCalledWith(
  323. err,
  324. child,
  325. 'callback for immediate watcher "foo"'
  326. )
  327. }).then(done)
  328. })
  329. it('should capture promise error from immediate watcher', done => {
  330. const spy = vi.fn()
  331. let child
  332. let err
  333. const Child = {
  334. data() {
  335. return {
  336. foo: 'foo'
  337. }
  338. },
  339. watch: {
  340. foo: {
  341. immediate: true,
  342. handler() {
  343. err = new Error('userImmediateWatcherCallback error')
  344. return Promise.reject(err)
  345. }
  346. }
  347. },
  348. created() {
  349. child = this
  350. },
  351. render() {}
  352. }
  353. new Vue({
  354. errorCaptured: spy,
  355. render: h => h(Child)
  356. }).$mount()
  357. waitForUpdate(() => {
  358. expect(spy).toHaveBeenCalledWith(
  359. err,
  360. child,
  361. 'callback for immediate watcher "foo" (Promise/async)'
  362. )
  363. expect(globalSpy).toHaveBeenCalledWith(
  364. err,
  365. child,
  366. 'callback for immediate watcher "foo" (Promise/async)'
  367. )
  368. }).then(done)
  369. })
  370. })