Skip to content

feat: expose everything on wrapper.vm to help testing script setup #931

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

Merged
merged 1 commit into from
Sep 24, 2021
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 3 additions & 20 deletions src/mount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,17 +337,6 @@ export function mount(
const Parent = defineComponent({
name: 'VTU_ROOT',
render() {
// https://github.com/vuejs/vue-test-utils-next/issues/651
// script setup components include an empty `expose` array as part of the
// code generated by the SFC compiler. Eg a component might look like
// { expose: [], setup: [Function], render: [Function] }
// not sure why (yet), but the empty expose array causes events to not be
// correctly captured.
// TODO: figure out why this is happening and understand the implications of
// the expose rfc for Test Utils.
if (isObjectComponent(component)) {
delete component.expose
}
return h(component, props, slots)
}
})
Expand Down Expand Up @@ -457,19 +446,13 @@ export function mount(
const warnSave = console.warn
console.warn = () => {}

// get `vm`.
// for some unknown reason, getting the `vm` for components using `<script setup>`
// as of Vue 3.0.3 works differently.
// if `appRef` has keys, use that (vm always has keys like $el, $props etc).
// if not, use the return value from app.mount.
const appRef = vm.$refs[MOUNT_COMPONENT_REF] as ComponentPublicInstance
const $vm = Reflect.ownKeys(appRef).length ? appRef : vm
// we add `hasOwnProperty` so jest can spy on the proxied vm without throwing
$vm.hasOwnProperty = (property) => {
return Reflect.has($vm, property)
appRef.hasOwnProperty = (property) => {
return Reflect.has(appRef, property)
}
console.warn = warnSave
return createWrapper(app, $vm, setProps)
return createWrapper(app, appRef, setProps)
}

export const shallowMount: typeof mount = (component: any, options?: any) => {
Expand Down
7 changes: 6 additions & 1 deletion src/vueWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ export class VueWrapper<T extends ComponentPublicInstance>
this.__app = app
// root is null on functional components
this.rootVM = vm?.$root
this.componentVM = vm as T
// vm.$.proxy is what the template has access to
// so even if the component is closed (as they are by default for `script setup`)
// a test will still be able to do something like
// `expect(wrapper.vm.count).toBe(1)`
// (note that vm can be null for functional components, hence the condition)
this.componentVM = vm ? (vm.$.proxy as T) : (vm as T)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could also consider alternatives like:

  • exposing the proxy on a different field, like wrapper.proxy, allowing to do expect(wrapper.proxy.count).toBe(2), without changing wrapper.vm
  • add a mounting option to expose everything:
const wrapper = mount(Hello, { expose: true });
expect(wrapper.vm.count).toBe(2); // vm would be proxy here
  • only do this for script setup components, or for components that do not expose anything

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expect(wrapper.proxy.count).toBe(2) doesn't seem ideal - unless you know Vue and script setup very well, it'd be hard to understand (and even document). I think tests should be written the same way, regardless of whether you are using script setup, etc.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I feel the same. I think testing different types of components should not change the way tests are written.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I totally support @cexbrayat point - but being a devil's advocate - component implementation specific already leaks into VTU: for example you can't use data() option for class components, you won't be able to use components param with functional components, etc. I'm a bit concerned that we are doing exceptions here, although script setup is designed to be "blackboxed"

this.__setProps = setProps

this.attachNativeEventListener()
Expand Down
23 changes: 23 additions & 0 deletions tests/components/DefineExpose.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<template>
<div id="root">
<div id="msg">{{ msg }}</div>
<div>{{ other }}</div>
</div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'

export default defineComponent({
name: 'Hello',

setup(props, { expose }) {
const other = ref('other')
expose({ other })
return {
msg: ref('Hello world'),
other
}
}
})
</script>
19 changes: 19 additions & 0 deletions tests/components/ScriptSetup_Expose.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script setup lang="ts">
// imported components are also directly usable in template
import { ref } from 'vue'
import Hello from './Hello.vue'

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

defineExpose({
count
})
</script>

<template>
<button @click="inc">{{ count }}</button>
<Hello />
</template>
40 changes: 40 additions & 0 deletions tests/expose.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { mount } from '../src'
import Hello from './components/Hello.vue'
import DefineExpose from './components/DefineExpose.vue'
import ScriptSetupExpose from './components/ScriptSetup_Expose.vue'
import ScriptSetup from './components/ScriptSetup.vue'

describe('expose', () => {
it('access vm on simple components', async () => {
const wrapper = mount(Hello)

expect(wrapper.vm.msg).toBe('Hello world')
})

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

// 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')
})

it('access vm with <script setup> and defineExpose()', async () => {
const wrapper = mount(ScriptSetupExpose)

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)
})

it('access vm with <script setup> even without defineExpose()', async () => {
const wrapper = mount(ScriptSetup)

await wrapper.find('button').trigger('click')
expect(wrapper.html()).toContain('1')
// can access `count` even if it is _not_ exposed
expect(wrapper.vm.count).toBe(1)
})
})
1 change: 0 additions & 1 deletion tests/vm.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { defineComponent, ref } from 'vue'

import { mount } from '../src'

describe('vm', () => {
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.volar.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"compilerOptions": {
"lib": ["DOM", "ES2020"],
"skipLibCheck": true
}
},
"exclude": ["tests/expose.spec.ts"]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

volar can't figure out that we magically expose more things on wrapper.vm. That might be an issue long term as users may run into it as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this isn't really something than can be fixed - it makes little sense for Volar to have something special for test utils.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. I think there was an issue even with regular defineExpose (which should work), but we'll see if that's really an issue and what we can do about it.

}