Skip to content

Commit

Permalink
fix: modify createVMProxy (#2125)
Browse files Browse the repository at this point in the history
resolves #2116
  • Loading branch information
jason-chang authored Jul 20, 2023
1 parent a304f59 commit e235e3a
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 31 deletions.
6 changes: 5 additions & 1 deletion src/vueWrapper.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit e235e3a

Please sign in to comment.