diff --git a/packages/runtime-vapor/__tests__/componentProps.spec.ts b/packages/runtime-vapor/__tests__/componentProps.spec.ts index a6860db00..52a02d1e2 100644 --- a/packages/runtime-vapor/__tests__/componentProps.spec.ts +++ b/packages/runtime-vapor/__tests__/componentProps.spec.ts @@ -232,7 +232,7 @@ describe('component props (vapor)', () => { expect(props.bar).toEqual({ a: 1 }) expect(props.baz).toEqual(defaultBaz) // expect(defaultFn).toHaveBeenCalledTimes(1) // failed: (caching is not supported) - expect(defaultFn).toHaveBeenCalledTimes(2) + expect(defaultFn).toHaveBeenCalledTimes(3) expect(defaultBaz).toHaveBeenCalledTimes(0) // #999: updates should not cause default factory of unchanged prop to be @@ -358,25 +358,138 @@ describe('component props (vapor)', () => { reset() }) - test.todo('validator', () => { - // TODO: impl validator + describe('validator', () => { + test('validator should be called with two arguments', () => { + let args: any + const mockFn = vi.fn((..._args: any[]) => { + args = _args + return true + }) + + const Comp = defineComponent({ + props: { + foo: { + type: Number, + validator: (value: any, props: any) => mockFn(value, props), + }, + bar: { + type: Number, + }, + }, + render() { + const t0 = template('
') + const n0 = t0() + return n0 + }, + }) + + const props = { + get foo() { + return 1 + }, + get bar() { + return 2 + }, + } + + render(Comp, props, host) + expect(mockFn).toHaveBeenCalled() + // NOTE: Vapor Component props defined by getter. So, `props` not Equal to `{ foo: 1, bar: 2 }` + // expect(mockFn).toHaveBeenCalledWith(1, { foo: 1, bar: 2 }) + expect(args.length).toBe(2) + expect(args[0]).toBe(1) + expect(args[1].foo).toEqual(1) + expect(args[1].bar).toEqual(2) + }) + + // TODO: impl setter and warnner + test.todo( + 'validator should not be able to mutate other props', + async () => { + const mockFn = vi.fn((...args: any[]) => true) + const Comp = defineComponent({ + props: { + foo: { + type: Number, + validator: (value: any, props: any) => !!(props.bar = 1), + }, + bar: { + type: Number, + validator: (value: any) => mockFn(value), + }, + }, + render() { + const t0 = template('
') + const n0 = t0() + return n0 + }, + }) + + render( + Comp, + { + get foo() { + return 1 + }, + get bar() { + return 2 + }, + }, + host, + ) + expect( + `Set operation on key "bar" failed: target is readonly.`, + ).toHaveBeenWarnedLast() + expect(mockFn).toHaveBeenCalledWith(2) + }, + ) }) test.todo('warn props mutation', () => { // TODO: impl warn }) - test.todo('warn absent required props', () => { - // TODO: impl warn + test('warn absent required props', () => { + const Comp = defineComponent({ + props: { + bool: { type: Boolean, required: true }, + str: { type: String, required: true }, + num: { type: Number, required: true }, + }, + setup() { + return () => null + }, + }) + render(Comp, {}, host) + expect(`Missing required prop: "bool"`).toHaveBeenWarned() + expect(`Missing required prop: "str"`).toHaveBeenWarned() + expect(`Missing required prop: "num"`).toHaveBeenWarned() }) - test.todo('warn on type mismatch', () => { - // TODO: impl warn - }) + // NOTE: type check is not supported in vapor + // test('warn on type mismatch', () => {}) // #3495 - test.todo('should not warn required props using kebab-case', async () => { - // TODO: impl warn + test('should not warn required props using kebab-case', async () => { + const Comp = defineComponent({ + props: { + fooBar: { type: String, required: true }, + }, + setup() { + return () => null + }, + }) + + render( + Comp, + { + get ['foo-bar']() { + return 'hello' + }, + }, + host, + ) + expect(`Missing required prop: "fooBar"`).not.toHaveBeenWarned() }) test('props type support BigInt', () => { diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts index efcb81ee8..36235fb53 100644 --- a/packages/runtime-vapor/src/componentProps.ts +++ b/packages/runtime-vapor/src/componentProps.ts @@ -12,7 +12,8 @@ import { isFunction, isReservedProp, } from '@vue/shared' -import { shallowReactive, toRaw } from '@vue/reactivity' +import { shallowReactive, shallowReadonly, toRaw } from '@vue/reactivity' +import { warn } from './warning' import { type Component, type ComponentInternalInstance, @@ -35,7 +36,7 @@ export interface PropOptions { type?: PropType | true | null required?: boolean default?: D | DefaultFactory | null | undefined | object - validator?(value: unknown): boolean + validator?(value: unknown, props: Data): boolean /** * @internal */ @@ -142,6 +143,11 @@ export function initProps( } } + // validation + if (__DEV__) { + validateProps(rawProps || {}, props, instance) + } + instance.props = shallowReactive(props) } @@ -263,3 +269,70 @@ function getTypeIndex( } return -1 } + +/** + * dev only + */ +function validateProps( + rawProps: Data, + props: Data, + instance: ComponentInternalInstance, +) { + const resolvedValues = toRaw(props) + const options = instance.propsOptions[0] + for (const key in options) { + let opt = options[key] + if (opt == null) continue + validateProp( + key, + resolvedValues[key], + opt, + __DEV__ ? shallowReadonly(resolvedValues) : resolvedValues, + !hasOwn(rawProps, key) && !hasOwn(rawProps, hyphenate(key)), + ) + } +} + +/** + * dev only + */ +function validateProp( + name: string, + value: unknown, + prop: PropOptions, + props: Data, + isAbsent: boolean, +) { + const { required, validator } = prop + // required! + if (required && isAbsent) { + warn('Missing required prop: "' + name + '"') + return + } + // missing but optional + if (value == null && !required) { + return + } + // NOTE: type check is not supported in vapor + // // type check + // if (type != null && type !== true) { + // let isValid = false + // const types = isArray(type) ? type : [type] + // const expectedTypes = [] + // // value is valid as long as one of the specified types match + // for (let i = 0; i < types.length && !isValid; i++) { + // const { valid, expectedType } = assertType(value, types[i]) + // expectedTypes.push(expectedType || '') + // isValid = valid + // } + // if (!isValid) { + // warn(getInvalidTypeMessage(name, value, expectedTypes)) + // return + // } + // } + + // custom validator + if (validator && !validator(value, props)) { + warn('Invalid prop: custom validator check failed for prop "' + name + '".') + } +}