Browse Source

test(runtime-vapor): errorHandling (#245)

Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
XiaoDong 1 year ago
parent
commit
8941779f9d
1 changed files with 565 additions and 0 deletions
  1. 565 0
      packages/runtime-vapor/__tests__/errorHandling.spec.ts

+ 565 - 0
packages/runtime-vapor/__tests__/errorHandling.spec.ts

@@ -0,0 +1,565 @@
+import type { Component } from '../src/component'
+import { type RefEl, setRef } from '../src/dom/templateRef'
+import { onErrorCaptured, onMounted } from '../src/apiLifecycle'
+import { createComponent } from '../src/apiCreateComponent'
+import { makeRender } from './_utils'
+import { template } from '../src/dom/template'
+import { watch, watchEffect } from '../src/apiWatch'
+import { nextTick } from '../src/scheduler'
+import { ref } from '@vue/reactivity'
+
+const define = makeRender()
+
+describe('error handling', () => {
+  test('propagation', () => {
+    const err = new Error('foo')
+    const fn = vi.fn()
+
+    const Comp: Component = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info, 'root')
+          return false
+        })
+
+        return createComponent(Child)
+      },
+    }
+
+    const Child: Component = {
+      name: 'Child',
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info, 'child')
+        })
+        return createComponent(GrandChild)
+      },
+    }
+
+    const GrandChild: Component = {
+      setup() {
+        onMounted(() => {
+          throw err
+        })
+      },
+    }
+
+    define(Comp).render()
+    expect(fn).toHaveBeenCalledTimes(2)
+    expect(fn).toHaveBeenCalledWith(err, 'mounted hook', 'root')
+    expect(fn).toHaveBeenCalledWith(err, 'mounted hook', 'child')
+  })
+
+  test('propagation stoppage', () => {
+    const err = new Error('foo')
+    const fn = vi.fn()
+
+    const Comp = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info, 'root')
+          return false
+        })
+        return createComponent(Child)
+      },
+    }
+
+    const Child = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info, 'child')
+          return false
+        })
+        return createComponent(GrandChild)
+      },
+    }
+
+    const GrandChild = {
+      setup() {
+        onMounted(() => {
+          throw err
+        })
+      },
+    }
+
+    define(Comp).render()
+    expect(fn).toHaveBeenCalledTimes(1)
+    expect(fn).toHaveBeenCalledWith(err, 'mounted hook', 'child')
+  })
+
+  test('async error handling', async () => {
+    const err = new Error('foo')
+    const fn = vi.fn()
+
+    const Comp = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info)
+          return false
+        })
+        return createComponent(Child)
+      },
+    }
+
+    const Child = {
+      setup() {
+        onMounted(async () => {
+          throw err
+        })
+      },
+    }
+
+    define(Comp).render()
+    expect(fn).not.toHaveBeenCalled()
+    await new Promise(r => setTimeout(r))
+    expect(fn).toHaveBeenCalledWith(err, 'mounted hook')
+  })
+
+  test('error thrown in onErrorCaptured', () => {
+    const err = new Error('foo')
+    const err2 = new Error('bar')
+    const fn = vi.fn()
+
+    const Comp = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info)
+          return false
+        })
+        return createComponent(Child)
+      },
+    }
+
+    const Child = {
+      setup() {
+        onErrorCaptured(() => {
+          throw err2
+        })
+        return createComponent(GrandChild)
+      },
+    }
+
+    const GrandChild = {
+      setup() {
+        onMounted(() => {
+          throw err
+        })
+      },
+    }
+
+    define(Comp).render()
+    expect(fn).toHaveBeenCalledTimes(2)
+    expect(fn).toHaveBeenCalledWith(err, 'mounted hook')
+    expect(fn).toHaveBeenCalledWith(err2, 'errorCaptured hook')
+  })
+
+  test('setup function', () => {
+    const err = new Error('foo')
+    const fn = vi.fn()
+
+    const Comp = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info)
+          return false
+        })
+        return createComponent(Child)
+      },
+    }
+
+    const Child = {
+      setup() {
+        throw err
+      },
+    }
+
+    define(Comp).render()
+    expect(fn).toHaveBeenCalledWith(err, 'setup function')
+  })
+
+  test('in render function', () => {
+    const err = new Error('foo')
+    const fn = vi.fn()
+
+    const Comp = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info)
+          return false
+        })
+        return createComponent(Child)
+      },
+    }
+
+    const Child = {
+      render() {
+        throw err
+      },
+    }
+
+    define(Comp).render()
+    expect(fn).toHaveBeenCalledWith(err, 'render function')
+  })
+
+  test('in function ref', () => {
+    const err = new Error('foo')
+    const ref = () => {
+      throw err
+    }
+    const fn = vi.fn()
+
+    const Comp = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info)
+          return false
+        })
+        return createComponent(Child)
+      },
+    }
+
+    const Child = {
+      render() {
+        const el = template('<div>')()
+        setRef(el as RefEl, ref)
+        return el
+      },
+    }
+
+    define(Comp).render()
+    expect(fn).toHaveBeenCalledWith(err, 'ref function')
+  })
+
+  test('in effect', () => {
+    const err = new Error('foo')
+    const fn = vi.fn()
+
+    const Comp = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info)
+          return false
+        })
+        return createComponent(Child)
+      },
+    }
+
+    const Child = {
+      setup() {
+        watchEffect(() => {
+          throw err
+        })
+      },
+    }
+
+    define(Comp).render()
+    expect(fn).toHaveBeenCalledWith(err, 'watcher callback')
+  })
+
+  test('in watch getter', () => {
+    const err = new Error('foo')
+    const fn = vi.fn()
+
+    const Comp = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info)
+          return false
+        })
+        return createComponent(Child)
+      },
+    }
+
+    const Child = {
+      setup() {
+        watch(
+          () => {
+            throw err
+          },
+          () => {},
+        )
+      },
+    }
+
+    define(Comp).render()
+    expect(fn).toHaveBeenCalledWith(err, 'watcher getter')
+  })
+
+  test('in watch callback', async () => {
+    const err = new Error('foo')
+    const fn = vi.fn()
+
+    const Comp = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info)
+          return false
+        })
+        return createComponent(Child)
+      },
+    }
+
+    const count = ref(0)
+    const Child = {
+      setup() {
+        watch(
+          () => count.value,
+          () => {
+            throw err
+          },
+        )
+      },
+    }
+
+    define(Comp).render()
+
+    count.value++
+    await nextTick()
+    expect(fn).toHaveBeenCalledWith(err, 'watcher callback')
+  })
+
+  test('in effect cleanup', async () => {
+    const err = new Error('foo')
+    const count = ref(0)
+    const fn = vi.fn()
+
+    const Comp = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info)
+          return false
+        })
+        return createComponent(Child)
+      },
+    }
+
+    const Child = {
+      setup() {
+        watchEffect(onCleanup => {
+          count.value
+          onCleanup(() => {
+            throw err
+          })
+        })
+      },
+    }
+
+    define(Comp).render()
+
+    count.value++
+    await nextTick()
+    expect(fn).toHaveBeenCalledWith(err, 'watcher cleanup function')
+  })
+
+  test('in component event handler via emit', () => {
+    const err = new Error('foo')
+    const fn = vi.fn()
+
+    const Comp = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info)
+          return false
+        })
+        return createComponent(Child, {
+          onFoo: () => {
+            throw err
+          },
+        })
+      },
+    }
+
+    const Child = {
+      setup(props: any, { emit }: any) {
+        emit('foo')
+      },
+    }
+
+    define(Comp).render()
+    expect(fn).toHaveBeenCalledWith(err, 'setup function')
+  })
+
+  test.todo('in component event handler via emit (async)', async () => {
+    const err = new Error('foo')
+    const fn = vi.fn()
+
+    const Comp = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info)
+          return false
+        })
+        return createComponent(Child, {
+          async onFoo() {
+            throw err
+          },
+        })
+      },
+    }
+
+    const Child = {
+      props: ['onFoo'],
+      setup(props: any, { emit }: any) {
+        emit('foo')
+      },
+    }
+
+    define(Comp).render()
+    await nextTick()
+    expect(fn).toHaveBeenCalledWith(err, 'setup function')
+  })
+
+  test.todo('in component event handler via emit (async + array)', async () => {
+    const err = new Error('foo')
+    const fn = vi.fn()
+
+    const res: Promise<any>[] = []
+    const createAsyncHandler = (p: Promise<any>) => () => {
+      res.push(p)
+      return p
+    }
+
+    const Comp = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info)
+          return false
+        })
+        return createComponent(Child, [
+          {
+            onFoo: () => {
+              createAsyncHandler(Promise.reject(err))
+              createAsyncHandler(Promise.resolve(1))
+            },
+          },
+        ])
+      },
+    }
+
+    const Child = {
+      setup(props: any, { emit }: any) {
+        emit('foo')
+      },
+    }
+
+    define(Comp).render()
+
+    try {
+      await Promise.all(res)
+    } catch (e: any) {
+      expect(e).toBe(err)
+    }
+    expect(fn).toHaveBeenCalledWith(err, 'component event handler')
+  })
+
+  it('should warn unhandled', () => {
+    const groupCollapsed = vi.spyOn(console, 'groupCollapsed')
+    groupCollapsed.mockImplementation(() => {})
+    const log = vi.spyOn(console, 'log')
+    log.mockImplementation(() => {})
+
+    const err = new Error('foo')
+    const fn = vi.fn()
+
+    const Comp = {
+      setup() {
+        onErrorCaptured((err, instance, info) => {
+          fn(err, info)
+        })
+        return createComponent(Child)
+      },
+    }
+
+    const Child = {
+      setup() {
+        throw err
+      },
+    }
+
+    let caughtError
+    try {
+      define(Comp).render()
+    } catch (caught) {
+      caughtError = caught
+    }
+    expect(fn).toHaveBeenCalledWith(err, 'setup function')
+    expect(
+      `Unhandled error during execution of setup function`,
+    ).toHaveBeenWarned()
+    expect(caughtError).toBe(err)
+
+    groupCollapsed.mockRestore()
+    log.mockRestore()
+  })
+
+  //# 3127
+  test.fails('handle error in watch & watchEffect', async () => {
+    const error1 = new Error('error1')
+    const error2 = new Error('error2')
+    const error3 = new Error('error3')
+    const error4 = new Error('error4')
+    const handler = vi.fn()
+
+    const app = define({
+      setup() {
+        const count = ref(1)
+        watch(
+          count,
+          () => {
+            throw error1
+          },
+          { immediate: true },
+        )
+        watch(
+          count,
+          async () => {
+            throw error2
+          },
+          { immediate: true },
+        )
+        watchEffect(() => {
+          throw error3
+        })
+        watchEffect(async () => {
+          throw error4
+        })
+      },
+    }).create()
+
+    app.app.config.errorHandler = handler
+    app.mount()
+
+    await nextTick()
+    expect(handler).toHaveBeenCalledWith(error1, {}, 'watcher callback')
+    expect(handler).toHaveBeenCalledWith(error2, {}, 'watcher callback')
+    expect(handler).toHaveBeenCalledWith(error3, {}, 'watcher callback')
+    expect(handler).toHaveBeenCalledWith(error4, {}, 'watcher callback')
+    expect(handler).toHaveBeenCalledTimes(4)
+  })
+
+  // #9574
+  test.fails('should pause tracking in error handler', async () => {
+    const error = new Error('error')
+    const x = ref(Math.random())
+
+    const handler = vi.fn(() => {
+      x.value
+      x.value = Math.random()
+    })
+
+    const app = define({
+      setup() {
+        throw error
+      },
+    }).create()
+
+    app.app.config.errorHandler = handler
+    app.mount()
+
+    await nextTick()
+    expect(handler).toHaveBeenCalledWith(error, {}, 'render function')
+    expect(handler).toHaveBeenCalledTimes(1)
+  })
+
+  // native event handler handling should be tested in respective renderers
+})