diff --git a/packages/runtime-vapor/__tests__/errorHandling.spec.ts b/packages/runtime-vapor/__tests__/errorHandling.spec.ts new file mode 100644 index 000000000..e58348fa7 --- /dev/null +++ b/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('
')() + 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[] = [] + const createAsyncHandler = (p: Promise) => () => { + 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 +})