Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: modify createVMProxy to fix issue 2116 #2125

Merged
merged 8 commits into from Jul 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/vueWrapper.ts
Expand Up @@ -26,7 +26,11 @@ function createVMProxy<T extends ComponentPublicInstance>(
): T {
return new Proxy(vm, {
get(vm, key, receiver) {
if (key in setupState) {
if (vm.$.exposed && vm.$.exposeProxy && key in vm.$.exposeProxy) {
// first if the key is exposed
return Reflect.get(vm.$.exposeProxy, key, receiver)
} else if (key in setupState) {
// second if the key is acccessible from the setupState
return Reflect.get(setupState, key, receiver)
} else {
// vm.$.ctx is the internal context of the vm
Expand Down
56 changes: 50 additions & 6 deletions tests/components/DefineExpose.vue
@@ -1,7 +1,6 @@
<template>
<div id="root">
<div id="msg">{{ msg }}</div>
<div>{{ other }}</div>
<div id="msg">{{ returnedState }}</div>
</div>
</template>

Expand All @@ -12,11 +11,56 @@ export default defineComponent({
name: 'Hello',

setup(props, { expose }) {
const other = ref('other')
expose({ other })
/* ------ Common Test Case ------ */
const exposedState1 = 'exposedState1'
const exposedState2 = 'exposedState2'

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';
}
/* ------ Common Test Case End ------ */

const stateNonExposedAndNonReturned = 'stateNonExposedAndNonReturned'
const stateNonExposedAndNonReturnedGetter = () => {
return stateNonExposedAndNonReturned;
}

const returnedState = ref('returnedState')

expose({
/* ------ Common Test Case ------ */
exposeObjectLiteral: 'exposeObjectLiteral',

exposedState1,
exposedState2Alias: exposedState2,
exposedState2Getter,

exposedRef,
exposedRefGetter,

exposedMethod1,
exposedMethod2Alias: exposedMethod2,
/* ------ Common Test Case End ------ */

stateNonExposedAndNonReturnedGetter,
})

return {
msg: ref('Hello world'),
other
returnedState,
}
}
})
Expand Down
46 changes: 42 additions & 4 deletions tests/components/DefineExposeWithRenderFunction.vue
Expand Up @@ -5,10 +5,48 @@ export default defineComponent({
name: 'Hello',

setup(props, { expose }) {
const other = ref('other')
const msg = ref('Hello world')
expose({ other })
return () => [h('div', msg.value), h('div', other.value)]
/* ------ Common Test Case ------ */
const exposedState1 = 'exposedState1'
const exposedState2 = 'exposedState2'

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';
}
/* ------ Common Test Case End ------ */

const refUseByRenderFnButNotExposed = ref('refUseByRenderFnButNotExposed')

expose({
/* ------ Common Test Case ------ */
exposeObjectLiteral: 'exposeObjectLiteral',

exposedState1,
exposedState2Alias: exposedState2,
exposedState2Getter,

exposedRef,
exposedRefGetter,

exposedMethod1,
exposedMethod2Alias: exposedMethod2,
/* ------ Common Test Case ------ */
})

return () => [h('div', refUseByRenderFnButNotExposed.value)]
}
})
</script>
48 changes: 47 additions & 1 deletion tests/components/ScriptSetup_Expose.vue
Expand Up @@ -3,21 +3,67 @@
import { ref } from 'vue'
import Hello from './Hello.vue'

/* ------ Common Test Case ------ */
const exposedState1 = 'exposedState1'
const exposedState2 = 'exposedState2'

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';
}
/* ------ Common Test Case End ------ */

const refNonExposed = ref('refNonExposed')
const refNonExposedGetter = () => {
return refNonExposed.value;
}

const count = ref(0)
const inc = () => {
count.value++
}

const resetCount = () => {
count.value = 0
}


defineExpose({
/* ------ Common Test Case ------ */
exposeObjectLiteral: 'exposeObjectLiteral',

exposedState1,
exposedState2Alias: exposedState2,
exposedState2Getter,

exposedRef,
exposedRefGetter,

exposedMethod1,
exposedMethod2Alias: exposedMethod2,
/* ------ Common Test Case End ------ */

count,
resetCount
resetCount,
refNonExposedGetter,
})
</script>

<template>
<button @click="inc">{{ count }}</button>
<div>{{ refNonExposed }}</div>
<Hello />
</template>
84 changes: 65 additions & 19 deletions tests/expose.spec.ts
@@ -1,51 +1,97 @@
import { describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import { mount } from '../src'
import Hello from './components/Hello.vue'

import DefineExpose from './components/DefineExpose.vue'
import DefineExposeWithRenderFunction from './components/DefineExposeWithRenderFunction.vue'
import ScriptSetupExpose from './components/ScriptSetup_Expose.vue'
import ScriptSetup from './components/ScriptSetup.vue'
import ScriptSetupWithProps from './components/ScriptSetupWithProps.vue'

describe('expose', () => {
it('access vm on simple components', async () => {
const wrapper = mount(Hello)
const commonTests = (vm: any) => {
// exposedState1 is exposed vie `expose` and aliased to `exposedState1`
expect(vm.exposedState1).toBe('exposedState1')
// exposedState2 is exposed vie `expose` and aliased to `exposedState2Alias`
expect(vm.exposedState2Alias).toBe('exposedState2')

// exposed state can be changed but will not affect the original state
vm.exposedState2Alias = 'newExposedState2'
expect(vm.exposedState2Alias).toBe('newExposedState2')
expect(vm.exposedState2Getter()).toBe('exposedState2')

// exposed ref can be changed and will affect the original ref
// @ts-ignore upstream issue, see https://github.com/vuejs/vue-next/issues/4397#issuecomment-957613874
expect(vm.exposedRef).toBe('exposedRef')
vm.exposedRef = 'newExposedRef'
expect(vm.exposedRef).toBe('newExposedRef')
expect(vm.exposedRefGetter()).toBe('newExposedRef')

expect(wrapper.vm.msg).toBe('Hello world')
})
// exposedMethod1 is exposed vie `expose`
expect(vm.exposedMethod1).not.toBe(undefined)
expect(vm.exposedMethod1()).toBe('result of exposedMethod1')

// exposedMethod2 is exposed vie `expose` and aliased to `exposedMethod2Alias`
expect(vm.exposedMethod2Alias).not.toBe(undefined)
expect(vm.exposedMethod2Alias()).toBe('result of exposedMethod2')
}

it('access vm on simple components with custom `expose`', async () => {
const wrapper = mount(DefineExpose)
const vm = wrapper.vm

commonTests(vm)

// returned state shuold be accessible
expect(vm.returnedState).toBe('returnedState')

// other is exposed vie `expose`
expect(wrapper.vm.other).toBe('other')
// can access `msg` even if not exposed
expect(wrapper.vm.msg).toBe('Hello world')
// non-exposed and non-returned state should not be accessible
expect(
(vm as unknown as { stateNonExposedAndNonReturned: undefined })
.stateNonExposedAndNonReturned
).toBe(undefined)
})

it('access vm on simple components with custom `expose` and a setup returning a render function', async () => {
const wrapper = mount(DefineExposeWithRenderFunction)
const vm = wrapper.vm

// other is exposed vie `expose`
// @ts-ignore upstream issue, see https://github.com/vuejs/vue-next/issues/4397#issuecomment-957613874
expect(wrapper.vm.other).toBe('other')
// can't access `msg` as it is not exposed
commonTests(vm)

// can't access `refUseByRenderFnButNotExposed` as it is not exposed
// and we are in a component with a setup returning a render function
expect((wrapper.vm as unknown as { msg: undefined }).msg).toBeUndefined()
expect(
(vm as unknown as { refUseByRenderFnButNotExposed: undefined })
.refUseByRenderFnButNotExposed
).toBeUndefined()
})

it('access vm with <script setup> and defineExpose()', async () => {
const wrapper = mount(ScriptSetupExpose)
const vm = wrapper.vm as unknown as {
inc: () => void
resetCount: () => void
count: number
refNonExposed: string
refNonExposedGetter: () => string
}

commonTests(vm)

await wrapper.find('button').trigger('click')
expect(wrapper.html()).toContain('1')
// can access `count` as it is exposed via `defineExpose()`
expect(wrapper.vm.count).toBe(1)

wrapper.vm.resetCount()

expect(wrapper.vm.count).toBe(0)
// can access state/method/ref as it is exposed via `defineExpose()`
expect(vm.count).toBe(1)
vm.resetCount()
expect(vm.count).toBe(0)

// non-exposed state/method/ref should be accessible
vm.inc()
expect(vm.count).toBe(1)
expect(vm.refNonExposed).toBe('refNonExposed')
vm.refNonExposed = 'newRefNonExposed'
expect(vm.refNonExposedGetter()).toBe('newRefNonExposed')
})

it('access vm with <script setup> even without defineExpose()', async () => {
Expand Down