test(vapor): errorHandling

This commit is contained in:
Evan You 2024-12-09 23:42:23 +08:00
parent 8540ee4af9
commit 527905a85b
No known key found for this signature in database
GPG Key ID: 00E9AB7A6704CE0A
5 changed files with 86 additions and 142 deletions

View File

@ -386,6 +386,10 @@ export interface GenericComponentInstance {
* @internal * @internal
*/ */
setupState?: Data setupState?: Data
/**
* @internal
*/
setupContext?: any
/** /**
* devtools access to additional info * devtools access to additional info
* @internal * @internal
@ -594,10 +598,6 @@ export interface ComponentInternalInstance extends GenericComponentInstance {
ceReload?: (newStyles?: string[]) => void ceReload?: (newStyles?: string[]) => void
// the rest are only for stateful components --------------------------------- // the rest are only for stateful components ---------------------------------
/**
* @internal
*/
setupContext: SetupContext | null
/** /**
* setup related * setup related
* @internal * @internal

View File

@ -223,7 +223,7 @@ export function baseEmit(
if (handler) { if (handler) {
callWithAsyncErrorHandling( callWithAsyncErrorHandling(
handler as Function, handler as Function | Function[],
instance, instance,
ErrorCodes.COMPONENT_EVENT_HANDLER, ErrorCodes.COMPONENT_EVENT_HANDLER,
args, args,

View File

@ -1,12 +1,15 @@
import type { Component } from '../src/_old/component' import {
import { type RefEl, setRef } from '../src/dom/templateRef' nextTick,
import { onErrorCaptured, onMounted } from '../src/_old/apiLifecycle' onErrorCaptured,
import { createComponent } from '../src/_old/apiCreateComponent' onMounted,
ref,
watch,
watchEffect,
} from '@vue/runtime-dom'
import { createComponent, setRef, template } from '../src'
import { makeRender } from './_utils' import { makeRender } from './_utils'
import { template } from '../src/dom/template' import type { VaporComponent } from '../src/component'
import { watch, watchEffect } from '../src/_old/apiWatch' import type { RefEl } from '../src/dom/templateRef'
import { nextTick } from '../src/_old/scheduler'
import { ref } from '@vue/reactivity'
const define = makeRender() const define = makeRender()
@ -15,7 +18,7 @@ describe('error handling', () => {
const err = new Error('foo') const err = new Error('foo')
const fn = vi.fn() const fn = vi.fn()
const Comp: Component = { const Comp: VaporComponent = {
setup() { setup() {
onErrorCaptured((err, instance, info) => { onErrorCaptured((err, instance, info) => {
fn(err, info, 'root') fn(err, info, 'root')
@ -26,7 +29,7 @@ describe('error handling', () => {
}, },
} }
const Child: Component = { const Child: VaporComponent = {
name: 'Child', name: 'Child',
setup() { setup() {
onErrorCaptured((err, instance, info) => { onErrorCaptured((err, instance, info) => {
@ -36,11 +39,12 @@ describe('error handling', () => {
}, },
} }
const GrandChild: Component = { const GrandChild: VaporComponent = {
setup() { setup() {
onMounted(() => { onMounted(() => {
throw err throw err
}) })
return []
}, },
} }
@ -54,7 +58,7 @@ describe('error handling', () => {
const err = new Error('foo') const err = new Error('foo')
const fn = vi.fn() const fn = vi.fn()
const Comp = { const Comp: VaporComponent = {
setup() { setup() {
onErrorCaptured((err, instance, info) => { onErrorCaptured((err, instance, info) => {
fn(err, info, 'root') fn(err, info, 'root')
@ -64,7 +68,7 @@ describe('error handling', () => {
}, },
} }
const Child = { const Child: VaporComponent = {
setup() { setup() {
onErrorCaptured((err, instance, info) => { onErrorCaptured((err, instance, info) => {
fn(err, info, 'child') fn(err, info, 'child')
@ -74,11 +78,12 @@ describe('error handling', () => {
}, },
} }
const GrandChild = { const GrandChild: VaporComponent = {
setup() { setup() {
onMounted(() => { onMounted(() => {
throw err throw err
}) })
return []
}, },
} }
@ -91,7 +96,7 @@ describe('error handling', () => {
const err = new Error('foo') const err = new Error('foo')
const fn = vi.fn() const fn = vi.fn()
const Comp = { const Comp: VaporComponent = {
setup() { setup() {
onErrorCaptured((err, instance, info) => { onErrorCaptured((err, instance, info) => {
fn(err, info) fn(err, info)
@ -101,11 +106,12 @@ describe('error handling', () => {
}, },
} }
const Child = { const Child: VaporComponent = {
setup() { setup() {
onMounted(async () => { onMounted(async () => {
throw err throw err
}) })
return []
}, },
} }
@ -120,7 +126,7 @@ describe('error handling', () => {
const err2 = new Error('bar') const err2 = new Error('bar')
const fn = vi.fn() const fn = vi.fn()
const Comp = { const Comp: VaporComponent = {
setup() { setup() {
onErrorCaptured((err, instance, info) => { onErrorCaptured((err, instance, info) => {
fn(err, info) fn(err, info)
@ -130,7 +136,7 @@ describe('error handling', () => {
}, },
} }
const Child = { const Child: VaporComponent = {
setup() { setup() {
onErrorCaptured(() => { onErrorCaptured(() => {
throw err2 throw err2
@ -139,11 +145,12 @@ describe('error handling', () => {
}, },
} }
const GrandChild = { const GrandChild: VaporComponent = {
setup() { setup() {
onMounted(() => { onMounted(() => {
throw err throw err
}) })
return []
}, },
} }
@ -175,6 +182,7 @@ describe('error handling', () => {
define(Comp).render() define(Comp).render()
expect(fn).toHaveBeenCalledWith(err, 'setup function') expect(fn).toHaveBeenCalledWith(err, 'setup function')
expect(`returned non-block value`).toHaveBeenWarned()
}) })
test('in render function', () => { test('in render function', () => {
@ -201,7 +209,7 @@ describe('error handling', () => {
expect(fn).toHaveBeenCalledWith(err, 'render function') expect(fn).toHaveBeenCalledWith(err, 'render function')
}) })
test('in function ref', () => { test.todo('in function ref', () => {
const err = new Error('foo') const err = new Error('foo')
const ref = () => { const ref = () => {
throw err throw err
@ -234,7 +242,7 @@ describe('error handling', () => {
const err = new Error('foo') const err = new Error('foo')
const fn = vi.fn() const fn = vi.fn()
const Comp = { const Comp: VaporComponent = {
setup() { setup() {
onErrorCaptured((err, instance, info) => { onErrorCaptured((err, instance, info) => {
fn(err, info) fn(err, info)
@ -244,11 +252,12 @@ describe('error handling', () => {
}, },
} }
const Child = { const Child: VaporComponent = {
setup() { setup() {
watchEffect(() => { watchEffect(() => {
throw err throw err
}) })
return []
}, },
} }
@ -270,7 +279,7 @@ describe('error handling', () => {
}, },
} }
const Child = { const Child: VaporComponent = {
setup() { setup() {
watch( watch(
() => { () => {
@ -278,6 +287,7 @@ describe('error handling', () => {
}, },
() => {}, () => {},
) )
return []
}, },
} }
@ -300,7 +310,7 @@ describe('error handling', () => {
} }
const count = ref(0) const count = ref(0)
const Child = { const Child: VaporComponent = {
setup() { setup() {
watch( watch(
() => count.value, () => count.value,
@ -308,6 +318,7 @@ describe('error handling', () => {
throw err throw err
}, },
) )
return []
}, },
} }
@ -333,7 +344,7 @@ describe('error handling', () => {
}, },
} }
const Child = { const Child: VaporComponent = {
setup() { setup() {
watchEffect(onCleanup => { watchEffect(onCleanup => {
count.value count.value
@ -341,6 +352,7 @@ describe('error handling', () => {
throw err throw err
}) })
}) })
return []
}, },
} }
@ -362,24 +374,25 @@ describe('error handling', () => {
return false return false
}) })
return createComponent(Child, { return createComponent(Child, {
onFoo: () => { onFoo: () => () => {
throw err throw err
}, },
}) })
}, },
} }
const Child = { const Child: VaporComponent = {
setup(props: any, { emit }: any) { setup(props: any, { emit }: any) {
emit('foo') emit('foo')
return []
}, },
} }
define(Comp).render() define(Comp).render()
expect(fn).toHaveBeenCalledWith(err, 'setup function') expect(fn).toHaveBeenCalledWith(err, 'component event handler')
}) })
test.todo('in component event handler via emit (async)', async () => { test('in component event handler via emit (async)', async () => {
const err = new Error('foo') const err = new Error('foo')
const fn = vi.fn() const fn = vi.fn()
@ -390,26 +403,27 @@ describe('error handling', () => {
return false return false
}) })
return createComponent(Child, { return createComponent(Child, {
async onFoo() { onFoo: () => async () => {
throw err throw err
}, },
}) })
}, },
} }
const Child = { const Child: VaporComponent = {
props: ['onFoo'], props: ['onFoo'],
setup(props: any, { emit }: any) { setup(props: any, { emit }: any) {
emit('foo') emit('foo')
return []
}, },
} }
define(Comp).render() define(Comp).render()
await nextTick() await nextTick()
expect(fn).toHaveBeenCalledWith(err, 'setup function') expect(fn).toHaveBeenCalledWith(err, 'component event handler')
}) })
test.todo('in component event handler via emit (async + array)', async () => { test('in component event handler via emit (async + array)', async () => {
const err = new Error('foo') const err = new Error('foo')
const fn = vi.fn() const fn = vi.fn()
@ -419,36 +433,33 @@ describe('error handling', () => {
return p return p
} }
const handlers = [
createAsyncHandler(Promise.reject(err)),
createAsyncHandler(Promise.resolve(1)),
]
const Comp = { const Comp = {
setup() { setup() {
onErrorCaptured((err, instance, info) => { onErrorCaptured((err, instance, info) => {
fn(err, info) fn(err, info)
return false return false
}) })
return createComponent(Child, [ return createComponent(Child, {
{ onFoo: () => handlers,
onFoo: () => { })
createAsyncHandler(Promise.reject(err))
createAsyncHandler(Promise.resolve(1))
},
},
])
}, },
} }
const Child = { const Child: VaporComponent = {
setup(props: any, { emit }: any) { setup(props: any, { emit }: any) {
emit('foo') emit('foo')
return []
}, },
} }
define(Comp).render() define(Comp).render()
try { await expect(() => Promise.all(res)).rejects.toThrowError()
await Promise.all(res)
} catch (e: any) {
expect(e).toBe(err)
}
expect(fn).toHaveBeenCalledWith(err, 'component event handler') expect(fn).toHaveBeenCalledWith(err, 'component event handler')
}) })
@ -523,6 +534,7 @@ describe('error handling', () => {
watchEffect(async () => { watchEffect(async () => {
throw error4 throw error4
}) })
return []
}, },
}).create() }).create()

View File

@ -1,78 +0,0 @@
import { makeRender } from './_utils'
import { template } from '../src/dom/template'
const define = makeRender()
describe('renderer: element', () => {
it('should create an element', () => {
const { html } = define({
render() {
return template(`<div>`)()
},
}).render()
expect(html()).toBe('<div></div>')
})
it('should create an element with props', () => {
const { html } = define({
render() {
return template(`<div id="foo" class="bar">`)()
},
}).render()
expect(html()).toBe('<div id="foo" class="bar"></div>')
})
it('should create an element with direct text children', () => {
const { html } = define({
render() {
return template(`<div>foo bar`)()
},
}).render()
expect(html()).toBe('<div>foo bar</div>')
})
it('should create an element with direct text children and props', () => {
const { html } = define({
render() {
return template(`<div id="foo">bar`)()
},
}).render()
expect(html()).toBe('<div id="foo">bar</div>')
})
it.fails('should update an element tag which is already mounted', () => {
const { html } = define({
render() {
return template(`<div>foo`)()
},
}).render()
expect(html()).toBe('<div>foo</div>')
define({
render() {
return template(`<span>foo`)()
},
}).render()
expect(html()).toBe('<span>foo</span>')
})
it.fails('should update element props which is already mounted', () => {
const { html } = define({
render() {
return template(`<div id="baz">foo`)()
},
}).render()
expect(html()).toBe('<div id="baz">foo</div>')
define({
render() {
return template(`<div id="baz" class="bar">foo`)()
},
}).render()
expect(html()).toBe('<div id="baz" class="bar">foo</div>')
})
})

View File

@ -4,12 +4,14 @@ import {
EffectScope, EffectScope,
type EmitFn, type EmitFn,
type EmitsOptions, type EmitsOptions,
ErrorCodes,
type GenericAppContext, type GenericAppContext,
type GenericComponentInstance, type GenericComponentInstance,
type LifecycleHook, type LifecycleHook,
type NormalizedPropsOptions, type NormalizedPropsOptions,
type ObjectEmitsOptions, type ObjectEmitsOptions,
type SuspenseBoundary, type SuspenseBoundary,
callWithErrorHandling,
currentInstance, currentInstance,
nextUid, nextUid,
popWarningContext, popWarningContext,
@ -125,12 +127,13 @@ export function createComponent(
} }
const setupFn = isFunction(component) ? component : component.setup const setupFn = isFunction(component) ? component : component.setup
const setupContext = (instance.setupContext =
setupFn && setupFn.length > 1 ? new SetupContext(instance) : null)
const setupResult = setupFn const setupResult = setupFn
? setupFn( ? callWithErrorHandling(setupFn, instance, ErrorCodes.SETUP_FUNCTION, [
instance.props, instance.props,
// @ts-expect-error setupContext,
setupFn.length > 1 ? new SetupContext(instance) : null, ]) || EMPTY_OBJ
) || EMPTY_OBJ
: EMPTY_OBJ : EMPTY_OBJ
if (__DEV__ && !isBlock(setupResult)) { if (__DEV__ && !isBlock(setupResult)) {
@ -187,14 +190,19 @@ export function createComponent(
* dev only * dev only
*/ */
export function devRender(instance: VaporComponentInstance): void { export function devRender(instance: VaporComponentInstance): void {
instance.block = instance.type.render!.call( instance.block =
null, callWithErrorHandling(
instance.setupState, instance.type.render!,
instance.props, instance,
instance.emit, ErrorCodes.RENDER_FUNCTION,
instance.attrs, [
instance.slots, instance.setupState,
) instance.props,
instance.emit,
instance.attrs,
instance.slots,
],
) || []
} }
const emptyContext: GenericAppContext = { const emptyContext: GenericAppContext = {
@ -257,6 +265,8 @@ export class VaporComponentInstance implements GenericComponentInstance {
ec?: LifecycleHook // LifecycleHooks.ERROR_CAPTURED ec?: LifecycleHook // LifecycleHooks.ERROR_CAPTURED
sp?: LifecycleHook<() => Promise<unknown>> // LifecycleHooks.SERVER_PREFETCH sp?: LifecycleHook<() => Promise<unknown>> // LifecycleHooks.SERVER_PREFETCH
setupContext?: SetupContext | null
// dev only // dev only
setupState?: Record<string, any> setupState?: Record<string, any>
devtoolsRawSetupState?: any devtoolsRawSetupState?: any