]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
test(vapor): componentProps
authorEvan You <evan@vuejs.org>
Mon, 9 Dec 2024 14:04:15 +0000 (22:04 +0800)
committerEvan You <evan@vuejs.org>
Mon, 9 Dec 2024 14:04:15 +0000 (22:04 +0800)
packages/runtime-vapor/__tests__/_utils.ts
packages/runtime-vapor/__tests__/component.spec.ts
packages/runtime-vapor/__tests__/componentProps.spec.ts
packages/runtime-vapor/src/apiCreateApp.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/componentProps.ts

index a049889da68f974fd1695233c4fb41ed207ce103..6d0f0ee11ac0d2242c650e1c465793d4ec51cffa 100644 (file)
@@ -1,11 +1,6 @@
 import { createVaporApp, defineVaporComponent } from '../src'
 import type { App } from '@vue/runtime-dom'
-import type {
-  ObjectVaporComponent,
-  VaporComponent,
-  VaporComponentInstance,
-  VaporSetupFn,
-} from '../src/component'
+import type { VaporComponent, VaporComponentInstance } from '../src/component'
 import type { RawProps } from '../src/componentProps'
 
 export interface RenderContext {
@@ -20,7 +15,7 @@ export interface RenderContext {
   html: () => string
 }
 
-export function makeRender<C = ObjectVaporComponent | VaporSetupFn>(
+export function makeRender<C = VaporComponent>(
   initHost = (): HTMLDivElement => {
     const host = document.createElement('div')
     host.setAttribute('id', 'host')
index d468af66711c3ea62e4ab31d5a006a9a4d2b6a6c..75d00347167b6b5f25eca9a7d5d5b0f14efd5049 100644 (file)
@@ -1,5 +1,5 @@
-import { ref, setText, template, watchEffect } from '../src/_old'
-import { describe, expect } from 'vitest'
+import { ref, watchEffect } from '@vue/runtime-dom'
+import { setText, template } from '../src'
 import { makeRender } from './_utils'
 
 const define = makeRender()
index dcc67b90e1a97fd2ead1cd861614381f78fb122d..520dd9ef724c551b7b7a93a3bd2dae20b769e7d5 100644 (file)
@@ -1,33 +1,37 @@
 // NOTE: This test is implemented based on the case of `runtime-core/__test__/componentProps.spec.ts`.
 
 import {
-  createComponent,
-  defineComponent,
-  getCurrentInstance,
+  // currentInstance,
+  inject,
   nextTick,
+  provide,
   ref,
-  setText,
-  template,
   toRefs,
   watch,
-  watchEffect,
-} from '../src/_old'
+} from '@vue/runtime-dom'
+import {
+  createComponent,
+  defineVaporComponent,
+  renderEffect,
+  setText,
+  template,
+} from '../src'
 import { makeRender } from './_utils'
+import type { RawProps } from '../src/componentProps'
 
 const define = makeRender<any>()
 
 describe('component: props', () => {
-  // NOTE: no proxy
   test('stateful', () => {
     let props: any
     let attrs: any
 
     const { render } = define({
       props: ['fooBar', 'barBaz'],
-      render() {
-        const instance = getCurrentInstance()!
-        props = instance.props
-        attrs = instance.attrs
+      setup(_props: any, { attrs: _attrs }: any) {
+        props = _props
+        attrs = _attrs
+        return []
       },
     })
 
@@ -51,17 +55,16 @@ describe('component: props', () => {
     expect(attrs).toEqual({ qux: 5 })
   })
 
-  test.fails('stateful with setup', () => {
+  test('stateful with setup', () => {
     let props: any
     let attrs: any
 
     const { render } = define({
       props: ['foo'],
       setup(_props: any, { attrs: _attrs }: any) {
-        return () => {
-          props = _props
-          attrs = _attrs
-        }
+        props = _props
+        attrs = _attrs
+        return []
       },
     })
 
@@ -82,12 +85,13 @@ describe('component: props', () => {
     let props: any
     let attrs: any
 
-    const { component: Comp, render } = define((_props: any) => {
-      const instance = getCurrentInstance()!
-      props = instance.props
-      attrs = instance.attrs
-      return {}
-    })
+    const { component: Comp, render } = define(
+      (_props: any, { attrs: _attrs }: any) => {
+        props = _props
+        attrs = _attrs
+        return []
+      },
+    )
     Comp.props = ['foo']
 
     render({ foo: () => 1, bar: () => 2 })
@@ -108,10 +112,9 @@ describe('component: props', () => {
     let attrs: any
 
     const { render } = define((_props: any, { attrs: _attrs }: any) => {
-      const instance = getCurrentInstance()!
-      props = instance.props
-      attrs = instance.attrs
-      return {}
+      props = _props
+      attrs = _attrs
+      return []
     })
 
     render({ foo: () => 1 })
@@ -134,9 +137,9 @@ describe('component: props', () => {
         baz: Boolean,
         qux: Boolean,
       },
-      render() {
-        const instance = getCurrentInstance()!
-        props = instance.props
+      setup(_props: any) {
+        props = _props
+        return []
       },
     })
 
@@ -151,7 +154,7 @@ describe('component: props', () => {
     expect(props.bar).toBe(true)
     expect(props.baz).toBe(true)
     expect(props.qux).toBe('ok')
-    // expect('type check failed for prop "qux"').toHaveBeenWarned()
+    expect('type check failed for prop "qux"').toHaveBeenWarned()
   })
 
   test('default value', () => {
@@ -172,9 +175,9 @@ describe('component: props', () => {
           default: defaultBaz,
         },
       },
-      render() {
-        const instance = getCurrentInstance()!
-        props = instance.props
+      setup(_props: any) {
+        props = _props
+        return []
       },
     })
 
@@ -214,18 +217,40 @@ describe('component: props', () => {
     // expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 2 times)
   })
 
-  test.todo('using inject in default value factory', () => {
-    // TODO: impl inject
+  test('using inject in default value factory', () => {
+    let props: any
+
+    const Child = defineVaporComponent({
+      props: {
+        test: {
+          default: () => inject('test', 'default'),
+        },
+      },
+      setup(_props) {
+        props = _props
+        return []
+      },
+    })
+
+    const { render } = define({
+      setup() {
+        provide('test', 'injected')
+        return createComponent(Child)
+      },
+    })
+
+    render()
+
+    expect(props.test).toBe('injected')
   })
 
   test('optimized props updates', async () => {
     const t0 = template('<div>')
     const { component: Child } = define({
       props: ['foo'],
-      render() {
-        const instance = getCurrentInstance()!
+      setup(props: any) {
         const n0 = t0()
-        watchEffect(() => setText(n0, instance.props.foo))
+        renderEffect(() => setText(n0, props.foo))
         return n0
       },
     })
@@ -278,7 +303,7 @@ describe('component: props', () => {
             type: Number,
           },
         },
-        render() {
+        setup() {
           return t0()
         },
       }).render(props)
@@ -286,46 +311,54 @@ describe('component: props', () => {
       expect(mockFn).toHaveBeenCalledWith(1, { foo: 1, bar: 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)
-        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!({
-          foo() {
-            return 1
+    test('validator should not be able to mutate other props', async () => {
+      const mockFn = vi.fn((...args: any[]) => true)
+      define({
+        props: {
+          foo: {
+            type: Number,
+            validator: (value: any, props: any) => !!(props.bar = 1),
           },
-          bar() {
-            return 2
+          bar: {
+            type: Number,
+            validator: (value: any) => mockFn(value),
           },
-        })
+        },
+        setup() {
+          const t0 = template('<div/>')
+          const n0 = t0()
+          return n0
+        },
+      }).render!({
+        foo() {
+          return 1
+        },
+        bar() {
+          return 2
+        },
+      })
 
-        expect(
-          `Set operation on key "bar" failed: taris readonly.`,
-        ).toHaveBeenWarnedLast()
-        expect(mockFn).toHaveBeenCalledWith(2)
-      },
-    )
+      expect(
+        `Set operation on key "bar" failed: target is readonly.`,
+      ).toHaveBeenWarnedLast()
+      expect(mockFn).toHaveBeenCalledWith(2)
+    })
   })
 
-  test.todo('warn props mutation', () => {
-    // TODO: impl warn
+  test('warn props mutation', () => {
+    let props: any
+    const { render } = define({
+      props: ['foo'],
+      setup(_props: any) {
+        props = _props
+        return []
+      },
+    })
+    render({ foo: () => 1 })
+    expect(props.foo).toBe(1)
+
+    props.foo = 2
+    expect(`Attempt to mutate prop "foo" failed`).toHaveBeenWarned()
   })
 
   test('warn absent required props', () => {
@@ -336,7 +369,7 @@ describe('component: props', () => {
         num: { type: Number, required: true },
       },
       setup() {
-        return () => null
+        return []
       },
     }).render()
     expect(`Missing required prop: "bool"`).toHaveBeenWarned()
@@ -354,7 +387,7 @@ describe('component: props', () => {
         fooBar: { type: String, required: true },
       },
       setup() {
-        return () => null
+        return []
       },
     }).render({
       ['foo-bar']: () => 'hello',
@@ -368,10 +401,9 @@ describe('component: props', () => {
       props: {
         foo: BigInt,
       },
-      render() {
-        const instance = getCurrentInstance()!
+      setup(props: any) {
         const n0 = t0()
-        watchEffect(() => setText(n0, instance.props.foo))
+        renderEffect(() => setText(n0, props.foo))
         return n0
       },
     }).render({
@@ -382,10 +414,44 @@ describe('component: props', () => {
   })
 
   // #3474
-  test.todo(
-    'should cache the value returned from the default factory to avoid unnecessary watcher trigger',
-    () => {},
-  )
+  test('should cache the value returned from the default factory to avoid unnecessary watcher trigger', async () => {
+    let count = 0
+
+    const { render, html } = define({
+      props: {
+        foo: {
+          type: Object,
+          default: () => ({ val: 1 }),
+        },
+        bar: Number,
+      },
+      setup(props: any) {
+        watch(
+          () => props.foo,
+          () => {
+            count++
+          },
+        )
+        const t0 = template('<h1></h1>')
+        const n0 = t0()
+        renderEffect(() => {
+          setText(n0, props.foo.val, props.bar)
+        })
+        return n0
+      },
+    })
+
+    const foo = ref()
+    const bar = ref(0)
+    render({ foo: () => foo.value, bar: () => bar.value })
+    expect(html()).toBe(`<h1>10</h1>`)
+    expect(count).toBe(0)
+
+    bar.value++
+    await nextTick()
+    expect(html()).toBe(`<h1>11</h1>`)
+    expect(count).toBe(0)
+  })
 
   // #3288
   test('declared prop key should be present even if not passed', async () => {
@@ -394,7 +460,6 @@ describe('component: props', () => {
     const passFoo = ref(false)
 
     const Comp: any = {
-      render() {},
       props: {
         foo: String,
       },
@@ -402,11 +467,14 @@ describe('component: props', () => {
         initialKeys = Object.keys(props)
         const { foo } = toRefs(props)
         watch(foo, changeSpy)
+        return []
       },
     }
 
     define(() =>
-      createComponent(Comp, [() => (passFoo.value ? { foo: () => 'ok' } : {})]),
+      createComponent(Comp, {
+        $: [() => (passFoo.value ? { foo: 'ok' } : {})],
+      } as RawProps),
     ).render()
 
     expect(initialKeys).toMatchObject(['foo'])
@@ -417,16 +485,17 @@ describe('component: props', () => {
 
   // #3371
   test.todo(`avoid double-setting props when casting`, async () => {
-    // TODO: proide, slots
+    // TODO: provide, slots
   })
 
-  // NOTE: type check is not supported
-  test.todo('support null in required + multiple-type declarations', () => {
+  test('support null in required + multiple-type declarations', () => {
     const { render } = define({
       props: {
         foo: { type: [Function, null], required: true },
       },
-      render() {},
+      setup() {
+        return []
+      },
     })
 
     expect(() => {
@@ -442,15 +511,11 @@ describe('component: props', () => {
   test('handling attr with undefined value', () => {
     const { render, host } = define({
       inheritAttrs: false,
-      render() {
-        const instance = getCurrentInstance()!
+      setup(_: any, { attrs }: any) {
         const t0 = template('<div></div>')
         const n0 = t0()
-        watchEffect(() =>
-          setText(
-            n0,
-            JSON.stringify(instance.attrs) + Object.keys(instance.attrs),
-          ),
+        renderEffect(() =>
+          setText(n0, JSON.stringify(attrs) + Object.keys(attrs)),
         )
         return n0
       },
@@ -471,7 +536,7 @@ describe('component: props', () => {
         type: String,
       },
     }
-    define({ props, render() {} }).render({ msg: () => 'test' })
+    define({ props, setup: () => [] }).render({ msg: () => 'test' })
 
     expect(Object.keys(props.msg).length).toBe(1)
   })
@@ -481,7 +546,7 @@ describe('component: props', () => {
       props: {
         $foo: String,
       },
-      render() {},
+      setup: () => [],
     })
 
     render({ msg: () => 'test' })
index 9d6f1a5b48ff4214b87ecc23eff49164c82eaa5b..fc976f7b5fc56fceed5cfe32e795254c35ee3a81 100644 (file)
@@ -30,12 +30,12 @@ const unmountApp: AppUnmountFn = app => {
   unmountComponent(app._instance as VaporComponentInstance, app._container)
 }
 
-export const createVaporApp: CreateAppFunction<
-  ParentNode,
-  VaporComponent
-> = comp => {
+export const createVaporApp: CreateAppFunction<ParentNode, VaporComponent> = (
+  comp,
+  props,
+) => {
   if (!_createApp) _createApp = createAppAPI(mountApp, unmountApp, i => i)
-  const app = _createApp(comp)
+  const app = _createApp(comp, props)
   const mount = app.mount
   app.mount = (container, ...args: any[]) => {
     container = normalizeContainer(container) as ParentNode
index 688fe29f51aeae48031aba0fa51c46595585552d..501a86b8a6770099dc076e8efaf2c0084396cee6 100644 (file)
@@ -307,8 +307,12 @@ export class VaporComponentInstance implements GenericComponentInstance {
     this.hasFallthrough = hasFallthroughAttrs(comp, rawProps)
     if (rawProps || comp.props) {
       const [propsHandlers, attrsHandlers] = getPropsProxyHandlers(comp)
-      this.props = comp.props ? new Proxy(this, propsHandlers!) : {}
       this.attrs = new Proxy(this, attrsHandlers)
+      this.props = comp.props
+        ? new Proxy(this, propsHandlers!)
+        : isFunction(comp)
+          ? this.attrs
+          : EMPTY_OBJ
     } else {
       this.props = this.attrs = EMPTY_OBJ
     }
index 8aa9e0cea4447c408a121787bc3949d6787a8f14..501770674b1ec7bda2b0bbd014f090c57886a247 100644 (file)
@@ -1,13 +1,24 @@
-import { EMPTY_ARR, NO, YES, camelize, hasOwn, isFunction } from '@vue/shared'
+import {
+  EMPTY_ARR,
+  NO,
+  YES,
+  camelize,
+  hasOwn,
+  isFunction,
+  isString,
+} from '@vue/shared'
 import type { VaporComponent, VaporComponentInstance } from './component'
 import {
   type NormalizedPropsOptions,
   baseNormalizePropsOptions,
+  currentInstance,
   isEmitListener,
   popWarningContext,
   pushWarningContext,
   resolvePropValue,
+  simpleSetCurrentInstance,
   validateProps,
+  warn,
 } from '@vue/runtime-dom'
 import { normalizeEmitsOptions } from './componentEmits'
 import { renderEffect } from './renderEffect'
@@ -39,15 +50,18 @@ export function getPropsProxyHandlers(
   }
   const propsOptions = normalizePropsOptions(comp)[0]
   const emitsOptions = normalizeEmitsOptions(comp)
-  const isProp = propsOptions
-    ? (key: string) => hasOwn(propsOptions, camelize(key))
-    : NO
+  const isProp = (
+    propsOptions
+      ? (key: string | symbol) =>
+          isString(key) && hasOwn(propsOptions, camelize(key))
+      : NO
+  ) as (key: string | symbol) => key is string
   const isAttr = propsOptions
     ? (key: string) =>
         key !== '$' && !isProp(key) && !isEmitListener(emitsOptions, key)
     : YES
 
-  const getProp = (instance: VaporComponentInstance, key: string) => {
+  const getProp = (instance: VaporComponentInstance, key: string | symbol) => {
     if (!isProp(key)) return
     const rawProps = instance.rawProps
     const dynamicSources = rawProps.$
@@ -94,9 +108,9 @@ export function getPropsProxyHandlers(
 
   const propsHandlers = propsOptions
     ? ({
-        get: (target, key: string) => getProp(target, key),
-        has: (_, key: string) => isProp(key),
-        getOwnPropertyDescriptor(target, key: string) {
+        get: (target, key) => getProp(target, key),
+        has: (_, key) => isProp(key),
+        getOwnPropertyDescriptor(target, key) {
           if (isProp(key)) {
             return {
               configurable: true,
@@ -106,11 +120,16 @@ export function getPropsProxyHandlers(
           }
         },
         ownKeys: () => Object.keys(propsOptions),
-        set: NO,
-        deleteProperty: NO,
       } satisfies ProxyHandler<VaporComponentInstance>)
     : null
 
+  if (__DEV__ && propsOptions) {
+    Object.assign(propsHandlers!, {
+      set: propsSetDevTrap,
+      deleteProperty: propsDeleteDevTrap,
+    })
+  }
+
   const getAttr = (target: RawProps, key: string) => {
     if (!isProp(key) && !isEmitListener(emitsOptions, key)) {
       return getAttrFromRawProps(target, key)
@@ -156,10 +175,15 @@ export function getPropsProxyHandlers(
       }
       return Array.from(new Set(keys))
     },
-    set: NO,
-    deleteProperty: NO,
   } satisfies ProxyHandler<VaporComponentInstance>
 
+  if (__DEV__) {
+    Object.assign(attrsHandlers, {
+      set: propsSetDevTrap,
+      deleteProperty: propsDeleteDevTrap,
+    })
+  }
+
   return (comp.__propsHandlers = [propsHandlers, attrsHandlers])
 }
 
@@ -217,7 +241,11 @@ function resolveDefault(
   factory: (props: Record<string, any>) => unknown,
   instance: VaporComponentInstance,
 ) {
-  return factory.call(null, instance.props)
+  const prev = currentInstance
+  simpleSetCurrentInstance(instance)
+  const res = factory.call(null, instance.props)
+  simpleSetCurrentInstance(prev, instance)
+  return res
 }
 
 export function hasFallthroughAttrs(
@@ -278,3 +306,17 @@ export function resolveDynamicProps(props: RawProps): Record<string, unknown> {
   }
   return mergedRawProps
 }
+
+function propsSetDevTrap(_: any, key: string | symbol) {
+  warn(
+    `Attempt to mutate prop ${JSON.stringify(key)} failed. Props are readonly.`,
+  )
+  return true
+}
+
+function propsDeleteDevTrap(_: any, key: string | symbol) {
+  warn(
+    `Attempt to delete prop ${JSON.stringify(key)} failed. Props are readonly.`,
+  )
+  return true
+}