Quellcode durchsuchen

test(runtime-vapor): component props (#99)

Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
ubugeeei vor 2 Jahren
Ursprung
Commit
ea5f7ec076

+ 1 - 1
packages/runtime-core/__tests__/componentProps.spec.ts

@@ -711,7 +711,7 @@ describe('component props', () => {
     )
   })
 
-  // #691ef
+  // #6915
   test('should not mutate original props long-form definition object', () => {
     const props = {
       msg: {

+ 487 - 0
packages/runtime-vapor/__tests__/componentProps.spec.ts

@@ -0,0 +1,487 @@
+// NOTE: This test is implemented based on the case of `runtime-core/__test__/componentProps.spec.ts`.
+
+// NOTE: not supported
+// mixins
+// caching
+
+import { type FunctionalComponent, setCurrentInstance } from '../src/component'
+import {
+  children,
+  defineComponent,
+  getCurrentInstance,
+  nextTick,
+  ref,
+  render,
+  setText,
+  template,
+  watchEffect,
+} from '../src'
+
+let host: HTMLElement
+const initHost = () => {
+  host = document.createElement('div')
+  host.setAttribute('id', 'host')
+  document.body.appendChild(host)
+}
+beforeEach(() => initHost())
+afterEach(() => host.remove())
+
+describe('component props (vapor)', () => {
+  test('stateful', () => {
+    let props: any
+    // TODO: attrs
+
+    const Comp = defineComponent({
+      props: ['fooBar', 'barBaz'],
+      render() {
+        const instance = getCurrentInstance()!
+        props = instance.props
+      },
+    })
+
+    render(
+      Comp,
+      {
+        get fooBar() {
+          return 1
+        },
+      },
+      host,
+    )
+    expect(props.fooBar).toEqual(1)
+
+    // test passing kebab-case and resolving to camelCase
+    render(
+      Comp,
+      {
+        get ['foo-bar']() {
+          return 2
+        },
+      },
+      host,
+    )
+    expect(props.fooBar).toEqual(2)
+
+    // test updating kebab-case should not delete it (#955)
+    render(
+      Comp,
+      {
+        get ['foo-bar']() {
+          return 3
+        },
+        get barBaz() {
+          return 5
+        },
+      },
+      host,
+    )
+    expect(props.fooBar).toEqual(3)
+    expect(props.barBaz).toEqual(5)
+
+    render(Comp, {}, host)
+    expect(props.fooBar).toBeUndefined()
+    expect(props.barBaz).toBeUndefined()
+    // expect(props.qux).toEqual(5) // TODO: attrs
+  })
+
+  test.todo('stateful with setup', () => {
+    // TODO:
+  })
+
+  test('functional with declaration', () => {
+    let props: any
+    // TODO: attrs
+
+    const Comp: FunctionalComponent = defineComponent((_props: any) => {
+      const instance = getCurrentInstance()!
+      props = instance.props
+      return {}
+    })
+    Comp.props = ['foo']
+    Comp.render = (() => {}) as any
+
+    render(
+      Comp,
+      {
+        get foo() {
+          return 1
+        },
+      },
+      host,
+    )
+    expect(props.foo).toEqual(1)
+
+    render(
+      Comp,
+      {
+        get foo() {
+          return 2
+        },
+      },
+      host,
+    )
+    expect(props.foo).toEqual(2)
+
+    render(Comp, {}, host)
+    expect(props.foo).toBeUndefined()
+  })
+
+  test('functional without declaration', () => {
+    let props: any
+    // TODO: attrs
+
+    const Comp: FunctionalComponent = defineComponent((_props: any) => {
+      const instance = getCurrentInstance()!
+      props = instance.props
+      return {}
+    })
+    Comp.props = undefined as any
+    Comp.render = (() => {}) as any
+
+    render(
+      Comp,
+      {
+        get foo() {
+          return 1
+        },
+      },
+      host,
+    )
+    expect(props.foo).toBeUndefined()
+
+    render(
+      Comp,
+      {
+        get foo() {
+          return 2
+        },
+      },
+      host,
+    )
+    expect(props.foo).toBeUndefined()
+  })
+
+  test('boolean casting', () => {
+    let props: any
+    const Comp = defineComponent({
+      props: {
+        foo: Boolean,
+        bar: Boolean,
+        baz: Boolean,
+        qux: Boolean,
+      },
+      render() {
+        const instance = getCurrentInstance()!
+        props = instance.props
+      },
+    })
+
+    render(
+      Comp,
+      {
+        // absent should cast to false
+        bar: '', // empty string should cast to true
+        baz: 'baz', // same string should cast to true
+        qux: 'ok', // other values should be left in-tact (but raise warning)
+      },
+      host,
+    )
+
+    expect(props.foo).toBe(false)
+    expect(props.bar).toBe(true)
+    expect(props.baz).toBe(true)
+    expect(props.qux).toBe('ok')
+  })
+
+  test('default value', () => {
+    let props: any
+    const defaultFn = vi.fn(() => ({ a: 1 }))
+    const defaultBaz = vi.fn(() => ({ b: 1 }))
+
+    const Comp = defineComponent({
+      props: {
+        foo: {
+          default: 1,
+        },
+        bar: {
+          default: defaultFn,
+        },
+        baz: {
+          type: Function,
+          default: defaultBaz,
+        },
+      },
+      render() {
+        const instance = getCurrentInstance()!
+        props = instance.props
+      },
+    })
+
+    render(
+      Comp,
+      {
+        get foo() {
+          return 2
+        },
+      },
+      host,
+    )
+    expect(props.foo).toBe(2)
+    // const prevBar = props.bar
+    props.bar
+    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(defaultBaz).toHaveBeenCalledTimes(0)
+
+    // #999: updates should not cause default factory of unchanged prop to be
+    // called again
+    render(
+      Comp,
+      {
+        get foo() {
+          return 3
+        },
+      },
+      host,
+    )
+    expect(props.foo).toBe(3)
+    expect(props.bar).toEqual({ a: 1 })
+    // expect(props.bar).toBe(prevBar) // failed: (caching is not supported)
+    // expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 3 times)
+
+    render(
+      Comp,
+      {
+        get bar() {
+          return { b: 2 }
+        },
+      },
+      host,
+    )
+    expect(props.foo).toBe(1)
+    expect(props.bar).toEqual({ b: 2 })
+    // expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 3 times)
+
+    render(
+      Comp,
+      {
+        get foo() {
+          return 3
+        },
+        get bar() {
+          return { b: 3 }
+        },
+      },
+      host,
+    )
+    expect(props.foo).toBe(3)
+    expect(props.bar).toEqual({ b: 3 })
+    // expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 3 times)
+
+    render(
+      Comp,
+      {
+        get bar() {
+          return { b: 4 }
+        },
+      },
+      host,
+    )
+    expect(props.foo).toBe(1)
+    expect(props.bar).toEqual({ b: 4 })
+    // expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 3 times)
+  })
+
+  test.todo('using inject in default value factory', () => {
+    // TODO: impl inject
+  })
+
+  // NOTE: maybe it's unnecessary
+  // https://github.com/vuejs/core-vapor/pull/99#discussion_r1472647377
+  test('optimized props updates', async () => {
+    const Child = defineComponent({
+      props: ['foo'],
+      render() {
+        const instance = getCurrentInstance()!
+        const t0 = template('<div><!></div>')
+        const n0 = t0()
+        const {
+          0: [n1],
+        } = children(n0)
+        watchEffect(() => {
+          setText(n1, instance.props.foo)
+        })
+        return n0
+      },
+    })
+
+    const foo = ref(1)
+    const id = ref('a')
+    const Comp = defineComponent({
+      setup() {
+        return { foo, id }
+      },
+      render(_ctx: Record<string, any>) {
+        const t0 = template('')
+        const n0 = t0()
+        render(
+          Child,
+          {
+            get foo() {
+              return _ctx.foo
+            },
+            get id() {
+              return _ctx.id
+            },
+          },
+          n0 as any, // TODO: type
+        )
+        return n0
+      },
+    })
+
+    const instace = render(Comp, {}, host)
+    const reset = setCurrentInstance(instace)
+    // expect(host.innerHTML).toBe('<div id="a">1</div>') // TODO: Fallthrough Attributes
+    expect(host.innerHTML).toBe('<div>1</div>')
+
+    foo.value++
+    await nextTick()
+    // expect(host.innerHTML).toBe('<div id="a">2</div>') // TODO: Fallthrough Attributes
+    expect(host.innerHTML).toBe('<div>2</div>')
+
+    // id.value = 'b'
+    // await nextTick()
+    // expect(host.innerHTML).toBe('<div id="b">2</div>') // TODO: Fallthrough Attributes
+    reset()
+  })
+
+  test.todo('validator', () => {
+    // TODO: impl validator
+  })
+
+  test.todo('warn props mutation', () => {
+    // TODO: impl warn
+  })
+
+  test.todo('warn absent required props', () => {
+    // TODO: impl warn
+  })
+
+  test.todo('warn on type mismatch', () => {
+    // TODO: impl warn
+  })
+
+  // #3495
+  test.todo('should not warn required props using kebab-case', async () => {
+    // TODO: impl warn
+  })
+
+  test('props type support BigInt', () => {
+    const Comp = defineComponent({
+      props: {
+        foo: BigInt,
+      },
+      render() {
+        const instance = getCurrentInstance()!
+        const t0 = template('<div></div>')
+        const n0 = t0()
+        const {
+          0: [n1],
+        } = children(n0)
+        watchEffect(() => {
+          setText(n1, instance.props.foo)
+        })
+        return n0
+      },
+    })
+
+    render(
+      Comp,
+      {
+        get foo() {
+          return (
+            BigInt(BigInt(100000111)) + BigInt(2000000000) * BigInt(30000000)
+          )
+        },
+      },
+      '#host',
+    )
+    expect(host.innerHTML).toBe('<div>60000000100000111</div>')
+  })
+
+  // #3288
+  test.todo(
+    'declared prop key should be present even if not passed',
+    async () => {
+      // let initialKeys: string[] = []
+      // const changeSpy = vi.fn()
+      // const passFoo = ref(false)
+      // const Comp = {
+      //   props: ['foo'],
+      //   setup() {
+      //     const instance = getCurrentInstance()!
+      //     initialKeys = Object.keys(instance.props)
+      //     watchEffect(changeSpy)
+      //     return {}
+      //   },
+      //   render() {
+      //     return {}
+      //   },
+      // }
+      // const Parent = createIf(
+      //   () => passFoo.value,
+      //   () => {
+      //     return render(Comp , { foo: 1 }, host) // TODO: createComponent fn
+      //   },
+      // )
+      // // expect(changeSpy).toHaveBeenCalledTimes(1)
+    },
+  )
+
+  // #3371
+  test.todo(`avoid double-setting props when casting`, async () => {
+    // TODO: proide, slots
+  })
+
+  test('support null in required + multiple-type declarations', () => {
+    const Comp = defineComponent({
+      props: {
+        foo: { type: [Function, null], required: true },
+      },
+      render() {},
+    })
+
+    expect(() => {
+      render(Comp, { foo: () => {} }, host)
+    }).not.toThrow()
+
+    expect(() => {
+      render(Comp, { foo: null }, host)
+    }).not.toThrow()
+  })
+
+  // #5016
+  test.todo('handling attr with undefined value', () => {
+    // TODO: attrs
+  })
+
+  // #6915
+  test('should not mutate original props long-form definition object', () => {
+    const props = {
+      msg: {
+        type: String,
+      },
+    }
+    const Comp = defineComponent({
+      props,
+      render() {},
+    })
+
+    render(Comp, { msg: 'test' }, host)
+
+    expect(Object.keys(props.msg).length).toBe(1)
+  })
+})

+ 8 - 10
packages/runtime-vapor/src/componentProps.ts

@@ -13,7 +13,11 @@ import {
   isReservedProp,
 } from '@vue/shared'
 import { shallowReactive, toRaw } from '@vue/reactivity'
-import type { Component, ComponentInternalInstance } from './component'
+import {
+  type Component,
+  type ComponentInternalInstance,
+  setCurrentInstance,
+} from './component'
 
 export type ComponentPropsOptions<P = Data> =
   | ComponentObjectPropsOptions<P>
@@ -165,15 +169,9 @@ function resolvePropValue(
         // if (key in propsDefaults) {
         //   value = propsDefaults[key]
         // } else {
-        //   setCurrentInstance(instance)
-        //   value = propsDefaults[key] = defaultValue.call(
-        //     __COMPAT__ &&
-        //       isCompatEnabled(DeprecationTypes.PROPS_DEFAULT_THIS, instance)
-        //       ? createPropsDefaultThis(instance, props, key)
-        //       : null,
-        //     props,
-        //   )
-        //   unsetCurrentInstance()
+        const reset = setCurrentInstance(instance)
+        value = defaultValue.call(null, props)
+        reset()
         // }
       } else {
         value = defaultValue