diff --git a/src/vueWrapper.ts b/src/vueWrapper.ts index d4eb17388..dde47a7ff 100644 --- a/src/vueWrapper.ts +++ b/src/vueWrapper.ts @@ -22,13 +22,17 @@ import { ShapeFlags } from './utils/vueShared' */ function createVMProxy( vm: T, - setupState: Record + setupState: Record, + exposed: Record | null ): T { return new Proxy(vm, { get(vm, key, receiver) { if (vm.$.exposed && vm.$.exposeProxy && key in vm.$.exposeProxy) { // first if the key is exposed return Reflect.get(vm.$.exposeProxy, key, receiver) + } else if (exposed && key in exposed) { + // first if the key is exposed + return Reflect.get(exposed, key, receiver) } else if (key in setupState) { // second if the key is acccessible from the setupState return Reflect.get(setupState, key, receiver) @@ -107,11 +111,31 @@ export class VueWrapper< // if we return it as `vm` // This does not work for functional components though (as they have no vm) // or for components with a setup that returns a render function (as they have an empty proxy) - // in both cases, we return `vm` directly instead + // in both cases, we return `vm` directly instead. + // + // NOTE https://github.com/vuejs/test-utils/issues/2591 + // I'm sry i'm not entirely sure why, but exposed properties — via expose/defineExpose + // are not assigned to the componentVM when the the `vm` argument provided + // to this constructor comes from `findComponent` — as in, not the original instance + // but already the proxied one. I suspect that is by design because according + // to the defineExpose docs, the exposed properties become "available for the + // parent component via templateRefs, which is the the case reported in the issue + // (in fact using templateRefs and doing .findComponent({ ref: 'refName' }) works + // as expected). But using "expose" via option or setup script does not keep + // this consistenticy and dependending on how setup return is done, the exposed + // might not be added to the vm even if exposed. + // So this can be considered highjacking the vuiedesign. + // It's up to the VTU to decide if it want to provide the convience of having + // a single interface without + // https://vuejs.org/api/sfc-script-setup.html#defineexpose + // https://vuejs.org/api/composition-api-setup.html#exposing-public-properties + // https://vuejs.org/api/options-state.html#expose + // if (hasSetupState(vm)) { - this.componentVM = createVMProxy(vm, vm.$.setupState) + this.componentVM = createVMProxy(vm, vm.$.setupState, vm.$.exposed) } else { this.componentVM = vm + Object.assign(this.componentVM, vm.$.exposed) } this.__setProps = setProps diff --git a/tests/components/DefineExpose.vue b/tests/components/DefineExpose.vue index 1beb34a99..f98b25f36 100644 --- a/tests/components/DefineExpose.vue +++ b/tests/components/DefineExpose.vue @@ -60,6 +60,7 @@ export default defineComponent({ }) return { + exposedMethod1, returnedState, } } diff --git a/tests/components/DefineExposeBundled.ts b/tests/components/DefineExposeBundled.ts new file mode 100644 index 000000000..5207dd4ea --- /dev/null +++ b/tests/components/DefineExposeBundled.ts @@ -0,0 +1,81 @@ +import { + defineComponent, + ref, + openBlock, + createElementBlock, + Fragment, + createElementVNode, + toDisplayString +} from 'vue' +const exposedState1 = 'exposedState1' +const exposedState2 = 'exposedState2' +const _sfc_main = /* @__PURE__ */ defineComponent({ + ...{ + name: 'Hello' + }, + __name: 'DefineExposeScriptSetup', + setup(__props, { expose: __expose }) { + const exposedState2Getter = () => { + return exposedState2 + } + const exposedRef = ref('exposedRef') + const exposedRefGetter = () => { + return exposedRef.value + } + const exposedMethod1 = () => { + return 'result of exposedMethod1' + } + const exposedMethod2 = () => { + return 'result of exposedMethod2' + } + const refNonExposed = ref('refNonExposed') + const refNonExposedGetter = () => { + return refNonExposed.value + } + const count = ref(0) + const inc = () => { + count.value++ + } + const resetCount = () => { + count.value = 0 + } + __expose({ + exposeObjectLiteral: 'exposeObjectLiteral', + exposedState1, + exposedState2Alias: exposedState2, + exposedState2Getter, + exposedRef, + exposedRefGetter, + exposedMethod1, + exposedMethod2Alias: exposedMethod2, + count, + resetCount, + refNonExposedGetter + }) + return (_ctx, _cache) => { + return ( + openBlock(), + createElementBlock( + Fragment, + null, + [ + createElementVNode( + 'button', + { onClick: inc }, + toDisplayString(count.value), + 1 + ), + createElementVNode( + 'div', + { 'force-expose': exposedMethod1 }, + toDisplayString(refNonExposed.value), + 1 + ) + ], + 64 + ) + ) + } + } +}) +export default _sfc_main diff --git a/tests/components/DefineExposeWithRenderFunction.vue b/tests/components/DefineExposeWithRenderFunction.vue index de161e2f2..5f1086a10 100644 --- a/tests/components/DefineExposeWithRenderFunction.vue +++ b/tests/components/DefineExposeWithRenderFunction.vue @@ -33,7 +33,7 @@ export default defineComponent({ expose({ /* ------ Common Test Case ------ */ exposeObjectLiteral: 'exposeObjectLiteral', - + exposedState1, exposedState2Alias: exposedState2, exposedState2Getter, @@ -46,7 +46,8 @@ export default defineComponent({ /* ------ Common Test Case ------ */ }) - return () => [h('div', refUseByRenderFnButNotExposed.value)] + return () => [ + h('div', refUseByRenderFnButNotExposed.value)] } }) diff --git a/tests/components/FindComponentExposeRenderFunction.js b/tests/components/FindComponentExposeRenderFunction.js new file mode 100644 index 000000000..48fe69bb5 --- /dev/null +++ b/tests/components/FindComponentExposeRenderFunction.js @@ -0,0 +1,21 @@ +import { defineComponent, h } from 'vue' + +export default defineComponent({ + name: 'FindComponentExposeRenderFunction', + props: { + someProp: String + }, + setup(_, { expose }) { + const exposedFn = () => { + return 'exposedFnReturn' + } + + expose({ + exposedFn + }) + + return () => { + return h('div', 'Example') + } + } +}) diff --git a/tests/components/FindComponentExposeScriptSetup.vue b/tests/components/FindComponentExposeScriptSetup.vue new file mode 100644 index 000000000..4992a198a --- /dev/null +++ b/tests/components/FindComponentExposeScriptSetup.vue @@ -0,0 +1,17 @@ + + + diff --git a/tests/components/FindComponentExposeScriptSetupBundled.ts b/tests/components/FindComponentExposeScriptSetupBundled.ts new file mode 100644 index 000000000..2c320d9b2 --- /dev/null +++ b/tests/components/FindComponentExposeScriptSetupBundled.ts @@ -0,0 +1,19 @@ +import { openBlock, createElementBlock } from 'vue' +const _sfc_main = { + __name: 'FindComponentExposeScriptSetupBundled', + props: { + someProp: String + }, + setup(__props, { expose: __expose }) { + const exposedFn = () => { + return 'exposedFnReturn' + } + __expose({ + exposedFn + }) + return (_ctx, _cache) => { + return openBlock(), createElementBlock('div', null, 'Example') + } + } +} +export default _sfc_main diff --git a/tests/components/FindComponentExposeTemplate.vue b/tests/components/FindComponentExposeTemplate.vue new file mode 100644 index 000000000..71d318f68 --- /dev/null +++ b/tests/components/FindComponentExposeTemplate.vue @@ -0,0 +1,28 @@ + + + + diff --git a/tests/components/ScriptSetup_Expose.vue b/tests/components/ScriptSetup_Expose.vue index 369d91138..ceb9820b1 100644 --- a/tests/components/ScriptSetup_Expose.vue +++ b/tests/components/ScriptSetup_Expose.vue @@ -60,6 +60,10 @@ defineExpose({ resetCount, refNonExposedGetter, }) + +defineOptions({ + name: 'Hello', +})