error-handling.spec.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. import Vue from 'vue'
  2. const components = createErrorTestComponents()
  3. describe('Error handling', () => {
  4. // hooks that prevents the component from rendering, but should not
  5. // break parent component
  6. ;[
  7. ['data', 'data()'],
  8. ['render', 'render'],
  9. ['beforeCreate', 'beforeCreate hook'],
  10. ['created', 'created hook'],
  11. ['beforeMount', 'beforeMount hook'],
  12. ['directive bind', 'directive foo bind hook'],
  13. ['event', 'event handler for "e"']
  14. ].forEach(([type, description]) => {
  15. it(`should recover from errors in ${type}`, done => {
  16. const vm = createTestInstance(components[type])
  17. expect(`Error in ${description}`).toHaveBeenWarned()
  18. expect(`Error: ${type}`).toHaveBeenWarned()
  19. assertRootInstanceActive(vm).then(done)
  20. })
  21. })
  22. // hooks that can return rejected promise
  23. ;[
  24. ['beforeCreate', 'beforeCreate hook'],
  25. ['created', 'created hook'],
  26. ['beforeMount', 'beforeMount hook'],
  27. ['mounted', 'mounted hook'],
  28. ['event', 'event handler for "e"']
  29. ].forEach(([type, description]) => {
  30. it(`should recover from promise errors in ${type}`, done => {
  31. createTestInstance(components[`${type}Async`])
  32. waitForUpdate(() => {
  33. expect(`Error in ${description} (Promise/async)`).toHaveBeenWarned()
  34. expect(`Error: ${type}`).toHaveBeenWarned()
  35. }).then(done)
  36. })
  37. })
  38. // error in mounted hook should affect neither child nor parent
  39. it('should recover from errors in mounted hook', done => {
  40. const vm = createTestInstance(components.mounted)
  41. expect(`Error in mounted hook`).toHaveBeenWarned()
  42. expect(`Error: mounted`).toHaveBeenWarned()
  43. assertBothInstancesActive(vm).then(done)
  44. })
  45. // error in beforeUpdate/updated should affect neither child nor parent
  46. ;[
  47. ['beforeUpdate', 'beforeUpdate hook'],
  48. ['updated', 'updated hook'],
  49. ['directive update', 'directive foo update hook']
  50. ].forEach(([type, description]) => {
  51. it(`should recover from errors in ${type} hook`, done => {
  52. const vm = createTestInstance(components[type])
  53. assertBothInstancesActive(vm)
  54. .then(() => {
  55. expect(`Error in ${description}`).toHaveBeenWarned()
  56. expect(`Error: ${type}`).toHaveBeenWarned()
  57. })
  58. .then(done)
  59. })
  60. })
  61. // hooks that can return rejected promise
  62. ;[
  63. ['beforeUpdate', 'beforeUpdate hook'],
  64. ['updated', 'updated hook']
  65. ].forEach(([type, description]) => {
  66. it(`should recover from promise errors in ${type} hook`, done => {
  67. const vm = createTestInstance(components[`${type}Async`])
  68. assertBothInstancesActive(vm)
  69. .then(() => {
  70. expect(`Error in ${description} (Promise/async)`).toHaveBeenWarned()
  71. expect(`Error: ${type}`).toHaveBeenWarned()
  72. })
  73. .then(done)
  74. })
  75. })
  76. ;[
  77. ['beforeDestroy', 'beforeDestroy hook'],
  78. ['destroyed', 'destroyed hook'],
  79. ['directive unbind', 'directive foo unbind hook']
  80. ].forEach(([type, description]) => {
  81. it(`should recover from errors in ${type} hook`, done => {
  82. const vm = createTestInstance(components[type])
  83. vm.ok = false
  84. waitForUpdate(() => {
  85. expect(`Error in ${description}`).toHaveBeenWarned()
  86. expect(`Error: ${type}`).toHaveBeenWarned()
  87. })
  88. .thenWaitFor(next => {
  89. assertRootInstanceActive(vm).end(next)
  90. })
  91. .then(done)
  92. })
  93. })
  94. ;[
  95. ['beforeDestroy', 'beforeDestroy hook'],
  96. ['destroyed', 'destroyed hook']
  97. ].forEach(([type, description]) => {
  98. it(`should recover from promise errors in ${type} hook`, done => {
  99. const vm = createTestInstance(components[`${type}Async`])
  100. vm.ok = false
  101. setTimeout(() => {
  102. expect(`Error in ${description} (Promise/async)`).toHaveBeenWarned()
  103. expect(`Error: ${type}`).toHaveBeenWarned()
  104. assertRootInstanceActive(vm).then(done)
  105. })
  106. })
  107. })
  108. it('should recover from errors in user watcher getter', done => {
  109. const vm = createTestInstance(components.userWatcherGetter)
  110. vm.n++
  111. waitForUpdate(() => {
  112. expect(`Error in getter for watcher`).toHaveBeenWarned()
  113. function getErrorMsg() {
  114. try {
  115. this.a.b.c
  116. } catch (e: any) {
  117. return e.toString()
  118. }
  119. }
  120. const msg = getErrorMsg.call(vm)
  121. expect(msg).toHaveBeenWarned()
  122. })
  123. .thenWaitFor(next => {
  124. assertBothInstancesActive(vm).end(next)
  125. })
  126. .then(done)
  127. })
  128. ;[
  129. ['userWatcherCallback', 'watcher'],
  130. ['userImmediateWatcherCallback', 'immediate watcher']
  131. ].forEach(([type, description]) => {
  132. it(`should recover from errors in user ${description} callback`, done => {
  133. const vm = createTestInstance(components[type])
  134. assertBothInstancesActive(vm)
  135. .then(() => {
  136. expect(`Error in callback for ${description} "n"`).toHaveBeenWarned()
  137. expect(`Error: ${type} error`).toHaveBeenWarned()
  138. })
  139. .then(done)
  140. })
  141. it(`should recover from promise errors in user ${description} callback`, done => {
  142. const vm = createTestInstance(components[`${type}Async`])
  143. assertBothInstancesActive(vm)
  144. .then(() => {
  145. expect(
  146. `Error in callback for ${description} "n" (Promise/async)`
  147. ).toHaveBeenWarned()
  148. expect(`Error: ${type} error`).toHaveBeenWarned()
  149. })
  150. .then(done)
  151. })
  152. })
  153. it('config.errorHandler should capture render errors', done => {
  154. const spy = (Vue.config.errorHandler = vi.fn())
  155. const vm = createTestInstance(components.render)
  156. const args = spy.mock.calls[0]
  157. expect(args[0].toString()).toContain('Error: render') // error
  158. expect(args[1]).toBe(vm.$refs.child) // vm
  159. expect(args[2]).toContain('render') // description
  160. assertRootInstanceActive(vm)
  161. .then(() => {
  162. Vue.config.errorHandler = undefined
  163. })
  164. .then(done)
  165. })
  166. it('should capture and recover from nextTick errors', done => {
  167. const err1 = new Error('nextTick')
  168. const err2 = new Error('nextTick2')
  169. const spy = (Vue.config.errorHandler = vi.fn())
  170. Vue.nextTick(() => {
  171. throw err1
  172. })
  173. Vue.nextTick(() => {
  174. expect(spy).toHaveBeenCalledWith(err1, undefined, 'nextTick')
  175. const vm = new Vue()
  176. vm.$nextTick(() => {
  177. throw err2
  178. })
  179. Vue.nextTick(() => {
  180. // should be called with correct instance info
  181. expect(spy).toHaveBeenCalledWith(err2, vm, 'nextTick')
  182. Vue.config.errorHandler = undefined
  183. done()
  184. })
  185. })
  186. })
  187. it('should recover from errors thrown in errorHandler itself', () => {
  188. Vue.config.errorHandler = () => {
  189. throw new Error('error in errorHandler ¯\\_(ツ)_/¯')
  190. }
  191. const vm = new Vue({
  192. render(h) {
  193. throw new Error('error in render')
  194. },
  195. renderError(h, err) {
  196. return h('div', err.toString())
  197. }
  198. }).$mount()
  199. expect('error in errorHandler').toHaveBeenWarned()
  200. expect('error in render').toHaveBeenWarned()
  201. expect(vm.$el.textContent).toContain('error in render')
  202. Vue.config.errorHandler = undefined
  203. })
  204. // event handlers that can throw errors or return rejected promise
  205. ;[
  206. ['single handler', '<div v-on:click="bork"></div>'],
  207. [
  208. 'multiple handlers',
  209. '<div v-on="{ click: [bork, function test() {}] }"></div>'
  210. ]
  211. ].forEach(([type, template]) => {
  212. it(`should recover from v-on errors for ${type} registered`, () => {
  213. const vm = new Vue({
  214. template,
  215. methods: {
  216. bork() {
  217. throw new Error('v-on')
  218. }
  219. }
  220. }).$mount()
  221. document.body.appendChild(vm.$el)
  222. global.triggerEvent(vm.$el, 'click')
  223. expect('Error in v-on handler').toHaveBeenWarned()
  224. expect('Error: v-on').toHaveBeenWarned()
  225. document.body.removeChild(vm.$el)
  226. })
  227. it(`should recover from v-on async errors for ${type} registered`, done => {
  228. const vm = new Vue({
  229. template,
  230. methods: {
  231. bork() {
  232. return new Promise((resolve, reject) =>
  233. reject(new Error('v-on async'))
  234. )
  235. }
  236. }
  237. }).$mount()
  238. document.body.appendChild(vm.$el)
  239. global.triggerEvent(vm.$el, 'click')
  240. waitForUpdate(() => {
  241. expect('Error in v-on handler (Promise/async)').toHaveBeenWarned()
  242. expect('Error: v-on').toHaveBeenWarned()
  243. document.body.removeChild(vm.$el)
  244. }).then(done)
  245. })
  246. })
  247. })
  248. function createErrorTestComponents() {
  249. const components: any = {}
  250. // data
  251. components.data = {
  252. data() {
  253. throw new Error('data')
  254. },
  255. render(h) {
  256. return h('div')
  257. }
  258. }
  259. // render error
  260. components.render = {
  261. render(h) {
  262. throw new Error('render')
  263. }
  264. }
  265. // lifecycle errors
  266. ;['create', 'mount', 'update', 'destroy'].forEach(hook => {
  267. // before
  268. const before = 'before' + hook.charAt(0).toUpperCase() + hook.slice(1)
  269. const beforeComp = (components[before] = {
  270. props: ['n'],
  271. render(h) {
  272. return h('div', this.n)
  273. }
  274. })
  275. beforeComp[before] = function () {
  276. throw new Error(before)
  277. }
  278. const beforeCompAsync = (components[`${before}Async`] = {
  279. props: ['n'],
  280. render(h) {
  281. return h('div', this.n)
  282. }
  283. })
  284. beforeCompAsync[before] = function () {
  285. return new Promise((resolve, reject) => reject(new Error(before)))
  286. }
  287. // after
  288. const after = hook.replace(/e?$/, 'ed')
  289. const afterComp = (components[after] = {
  290. props: ['n'],
  291. render(h) {
  292. return h('div', this.n)
  293. }
  294. })
  295. afterComp[after] = function () {
  296. throw new Error(after)
  297. }
  298. const afterCompAsync = (components[`${after}Async`] = {
  299. props: ['n'],
  300. render(h) {
  301. return h('div', this.n)
  302. }
  303. })
  304. afterCompAsync[after] = function () {
  305. return new Promise((resolve, reject) => reject(new Error(after)))
  306. }
  307. })
  308. // directive hooks errors
  309. ;['bind', 'update', 'unbind'].forEach(hook => {
  310. const key = 'directive ' + hook
  311. const dirComp: any = (components[key] = {
  312. props: ['n'],
  313. template: `<div v-foo="n">{{ n }}</div>`
  314. })
  315. const dirFoo = {}
  316. dirFoo[hook] = function () {
  317. throw new Error(key)
  318. }
  319. dirComp.directives = {
  320. foo: dirFoo
  321. }
  322. })
  323. // user watcher
  324. components.userWatcherGetter = {
  325. props: ['n'],
  326. created() {
  327. this.$watch(
  328. function () {
  329. return this.n + this.a.b.c
  330. },
  331. val => {
  332. console.log('user watcher fired: ' + val)
  333. }
  334. )
  335. },
  336. render(h) {
  337. return h('div', this.n)
  338. }
  339. }
  340. components.userWatcherCallback = {
  341. props: ['n'],
  342. watch: {
  343. n() {
  344. throw new Error('userWatcherCallback error')
  345. }
  346. },
  347. render(h) {
  348. return h('div', this.n)
  349. }
  350. }
  351. components.userImmediateWatcherCallback = {
  352. props: ['n'],
  353. watch: {
  354. n: {
  355. immediate: true,
  356. handler() {
  357. throw new Error('userImmediateWatcherCallback error')
  358. }
  359. }
  360. },
  361. render(h) {
  362. return h('div', this.n)
  363. }
  364. }
  365. components.userWatcherCallbackAsync = {
  366. props: ['n'],
  367. watch: {
  368. n() {
  369. return Promise.reject(new Error('userWatcherCallback error'))
  370. }
  371. },
  372. render(h) {
  373. return h('div', this.n)
  374. }
  375. }
  376. components.userImmediateWatcherCallbackAsync = {
  377. props: ['n'],
  378. watch: {
  379. n: {
  380. immediate: true,
  381. handler() {
  382. return Promise.reject(new Error('userImmediateWatcherCallback error'))
  383. }
  384. }
  385. },
  386. render(h) {
  387. return h('div', this.n)
  388. }
  389. }
  390. // event errors
  391. components.event = {
  392. beforeCreate() {
  393. this.$on('e', () => {
  394. throw new Error('event')
  395. })
  396. },
  397. mounted() {
  398. this.$emit('e')
  399. },
  400. render(h) {
  401. return h('div')
  402. }
  403. }
  404. components.eventAsync = {
  405. beforeCreate() {
  406. this.$on(
  407. 'e',
  408. () => new Promise((resolve, reject) => reject(new Error('event')))
  409. )
  410. },
  411. mounted() {
  412. this.$emit('e')
  413. },
  414. render(h) {
  415. return h('div')
  416. }
  417. }
  418. return components
  419. }
  420. function createTestInstance(Comp) {
  421. return new Vue({
  422. data: {
  423. n: 0,
  424. ok: true
  425. },
  426. render(h) {
  427. return h('div', [
  428. 'n:' + this.n + '\n',
  429. this.ok ? h(Comp, { ref: 'child', props: { n: this.n } }) : null
  430. ])
  431. }
  432. }).$mount()
  433. }
  434. function assertRootInstanceActive(vm) {
  435. expect(vm.$el.innerHTML).toContain('n:0\n')
  436. vm.n++
  437. return waitForUpdate(() => {
  438. expect(vm.$el.innerHTML).toContain('n:1\n')
  439. })
  440. }
  441. function assertBothInstancesActive(vm) {
  442. vm.n = 0
  443. return waitForUpdate(() => {
  444. expect(vm.$refs.child.$el.innerHTML).toContain('0')
  445. }).thenWaitFor(next => {
  446. assertRootInstanceActive(vm)
  447. .then(() => {
  448. expect(vm.$refs.child.$el.innerHTML).toContain('1')
  449. })
  450. .end(next)
  451. })
  452. }