Kaynağa Gözat

feat(runtime-vapor): component props validator (#114)

ubugeeei 2 yıl önce
ebeveyn
işleme
e9e7fe414b

+ 123 - 10
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('<div/>')
+          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('<div/>')
+            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', () => {

+ 75 - 2
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<T = any, D = T> {
   type?: PropType<T> | true | null
   required?: boolean
   default?: D | DefaultFactory<D> | 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 + '".')
+  }
+}