Skip to content

Commit f2b9091

Browse files
authored
fix: allow to mock global function in script setup components (#1871)
Fixes #1869
1 parent 046dacb commit f2b9091

File tree

8 files changed

+114
-14
lines changed

8 files changed

+114
-14
lines changed

src/mount.ts

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -478,16 +478,35 @@ export function mount(
478478
if (global?.mocks) {
479479
const mixin = defineComponent({
480480
beforeCreate() {
481-
for (const [k, v] of Object.entries(
482-
global.mocks as { [key: string]: any }
483-
)) {
484-
// we need to differentiate components that are or not not `script setup`
485-
// otherwise we run into a proxy set error
486-
// due to https://github.com/vuejs/core/commit/f73925d76a76ee259749b8b48cb68895f539a00f#diff-ea4d1ddabb7e22e17e80ada458eef70679af4005df2a1a6b73418fec897603ceR404
487-
// introduced in Vue v3.2.45
488-
if (hasSetupState(this)) {
489-
this.$.setupState[k] = v
490-
} else {
481+
// we need to differentiate components that are or not not `script setup`
482+
// otherwise we run into a proxy set error
483+
// due to https://github.com/vuejs/core/commit/f73925d76a76ee259749b8b48cb68895f539a00f#diff-ea4d1ddabb7e22e17e80ada458eef70679af4005df2a1a6b73418fec897603ceR404
484+
// introduced in Vue v3.2.45
485+
if (hasSetupState(this)) {
486+
// add the mocks to setupState
487+
for (const [k, v] of Object.entries(
488+
global.mocks as { [key: string]: any }
489+
)) {
490+
// we do this in a try/catch, as some properties might be read-only
491+
try {
492+
this.$.setupState[k] = v
493+
// eslint-disable-next-line no-empty
494+
} catch (e) {}
495+
}
496+
// also intercept the proxy calls to make the mocks available on the instance
497+
// (useful when a template access a global function like $t and the developer wants to mock it)
498+
;(this.$ as any).proxy = new Proxy((this.$ as any).proxy, {
499+
get(target, key) {
500+
if (key in global.mocks) {
501+
return global.mocks[key as string]
502+
}
503+
return target[key]
504+
}
505+
})
506+
} else {
507+
for (const [k, v] of Object.entries(
508+
global.mocks as { [key: string]: any }
509+
)) {
491510
;(this as any)[k] = v
492511
}
493512
}
@@ -604,8 +623,10 @@ export function mount(
604623
const appRef = componentRef.value! as ComponentPublicInstance
605624
// we add `hasOwnProperty` so Jest can spy on the proxied vm without throwing
606625
// note that this is not necessary with Jest v27+ or Vitest, but is kept for compatibility with older Jest versions
607-
appRef.hasOwnProperty = (property) => {
608-
return Reflect.has(appRef, property)
626+
if (!app.hasOwnProperty) {
627+
appRef.hasOwnProperty = (property) => {
628+
return Reflect.has(appRef, property)
629+
}
609630
}
610631
const wrapper = createVueWrapper(app, appRef, setProps)
611632
trackInstance(wrapper)

src/utils.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ export const mergeDeep = (
8080
if (!isObject(target) || !isObject(source)) {
8181
return source
8282
}
83-
8483
Object.keys(source).forEach((key) => {
8584
const targetValue = target[key]
8685
const sourceValue = source[key]
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<script lang="ts">
2+
import { defineComponent } from 'vue'
3+
4+
export default defineComponent({
5+
data() {
6+
return { hello: 'hello' }
7+
}
8+
})
9+
</script>
10+
11+
<template>
12+
<div>{{ hello }}</div>
13+
<!-- this emulates components that use a global function like $t for i18n -->
14+
<!-- this function can be mocked using global.mocks -->
15+
<div>{{ $t('world') }}</div>
16+
</template>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<script setup lang="ts">
2+
import { computed, ref } from 'vue'
3+
4+
const props = defineProps<{ count: number }>()
5+
6+
const times = ref(2)
7+
const result = computed(() => props.count * times.value)
8+
9+
defineExpose(props)
10+
</script>
11+
12+
<template>
13+
<div>{{ count }} x {{ times }} = {{ result }}</div>
14+
<button @click="times += 1">
15+
x1
16+
</button>
17+
</template>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script setup lang="ts">
2+
import { ref } from 'vue'
3+
const hello = ref('hello')
4+
</script>
5+
6+
<template>
7+
<div>{{ hello }}</div>
8+
<!-- this emulates components that use a global function like $t for i18n -->
9+
<!-- this function can be mocked using global.mocks -->
10+
<div>{{ $t('world') }}</div>
11+
</template>

tests/mount.spec.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { describe, expect, it } from 'vitest'
1+
import { describe, expect, it, vi } from 'vitest'
22
import { defineComponent } from 'vue'
33
import { mount } from '../src'
4+
import HelloFromVitestPlayground from './components/HelloFromVitestPlayground.vue'
45

56
describe('mount: general tests', () => {
67
it('correctly handles component, throwing on mount', () => {
@@ -23,4 +24,12 @@ describe('mount: general tests', () => {
2324

2425
expect(wrapper.html()).toBe('<div>hello</div>')
2526
})
27+
28+
it('should not warn on readonly hasOwnProperty when mounting a component', () => {
29+
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {})
30+
31+
mount(HelloFromVitestPlayground, { props: { count: 2 } })
32+
33+
expect(spy).not.toHaveBeenCalled()
34+
})
2635
})

tests/mountingOptions/mocks.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { describe, expect, it, vi } from 'vitest'
22
import { mount, RouterLinkStub } from '../../src'
33
import { defineComponent } from 'vue'
4+
import ScriptSetupWithI18n from '../components/ScriptSetupWithI18n.vue'
5+
import ComponentWithI18n from '../components/ComponentWithI18n.vue'
46

57
describe('mocks', () => {
68
it('mocks a vuex store', async () => {
@@ -75,4 +77,28 @@ describe('mocks', () => {
7577
await wrapper.find('button').trigger('click')
7678
expect($router.push).toHaveBeenCalledWith('/posts/1')
7779
})
80+
81+
it('mocks a global function in a script setup component', () => {
82+
const wrapper = mount(ScriptSetupWithI18n, {
83+
global: {
84+
mocks: {
85+
$t: () => 'mocked'
86+
}
87+
}
88+
})
89+
expect(wrapper.text()).toContain('hello')
90+
expect(wrapper.text()).toContain('mocked')
91+
})
92+
93+
it('mocks a global function in an option component', () => {
94+
const wrapper = mount(ComponentWithI18n, {
95+
global: {
96+
mocks: {
97+
$t: () => 'mocked'
98+
}
99+
}
100+
})
101+
expect(wrapper.text()).toContain('hello')
102+
expect(wrapper.text()).toContain('mocked')
103+
})
78104
})

types/testing.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ import type { Router } from 'vue-router'
33
declare module '@vue/runtime-core' {
44
interface ComponentCustomProperties {
55
$router: Router
6+
$t: (key: string) => string
67
}
78
}

0 commit comments

Comments
 (0)