Skip to content

Commit

Permalink
fix: allow to mock global function in script setup components (#1871)
Browse files Browse the repository at this point in the history
Fixes #1869
  • Loading branch information
cexbrayat authored Nov 20, 2022
1 parent 046dacb commit f2b9091
Show file tree
Hide file tree
Showing 8 changed files with 114 additions and 14 deletions.
45 changes: 33 additions & 12 deletions src/mount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,16 +478,35 @@ export function mount(
if (global?.mocks) {
const mixin = defineComponent({
beforeCreate() {
for (const [k, v] of Object.entries(
global.mocks as { [key: string]: any }
)) {
// we need to differentiate components that are or not not `script setup`
// otherwise we run into a proxy set error
// due to https://github.com/vuejs/core/commit/f73925d76a76ee259749b8b48cb68895f539a00f#diff-ea4d1ddabb7e22e17e80ada458eef70679af4005df2a1a6b73418fec897603ceR404
// introduced in Vue v3.2.45
if (hasSetupState(this)) {
this.$.setupState[k] = v
} else {
// we need to differentiate components that are or not not `script setup`
// otherwise we run into a proxy set error
// due to https://github.com/vuejs/core/commit/f73925d76a76ee259749b8b48cb68895f539a00f#diff-ea4d1ddabb7e22e17e80ada458eef70679af4005df2a1a6b73418fec897603ceR404
// introduced in Vue v3.2.45
if (hasSetupState(this)) {
// add the mocks to setupState
for (const [k, v] of Object.entries(
global.mocks as { [key: string]: any }
)) {
// we do this in a try/catch, as some properties might be read-only
try {
this.$.setupState[k] = v
// eslint-disable-next-line no-empty
} catch (e) {}
}
// also intercept the proxy calls to make the mocks available on the instance
// (useful when a template access a global function like $t and the developer wants to mock it)
;(this.$ as any).proxy = new Proxy((this.$ as any).proxy, {
get(target, key) {
if (key in global.mocks) {
return global.mocks[key as string]
}
return target[key]
}
})
} else {
for (const [k, v] of Object.entries(
global.mocks as { [key: string]: any }
)) {
;(this as any)[k] = v
}
}
Expand Down Expand Up @@ -604,8 +623,10 @@ export function mount(
const appRef = componentRef.value! as ComponentPublicInstance
// we add `hasOwnProperty` so Jest can spy on the proxied vm without throwing
// note that this is not necessary with Jest v27+ or Vitest, but is kept for compatibility with older Jest versions
appRef.hasOwnProperty = (property) => {
return Reflect.has(appRef, property)
if (!app.hasOwnProperty) {
appRef.hasOwnProperty = (property) => {
return Reflect.has(appRef, property)
}
}
const wrapper = createVueWrapper(app, appRef, setProps)
trackInstance(wrapper)
Expand Down
1 change: 0 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ export const mergeDeep = (
if (!isObject(target) || !isObject(source)) {
return source
}

Object.keys(source).forEach((key) => {
const targetValue = target[key]
const sourceValue = source[key]
Expand Down
16 changes: 16 additions & 0 deletions tests/components/ComponentWithI18n.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
data() {
return { hello: 'hello' }
}
})
</script>

<template>
<div>{{ hello }}</div>
<!-- this emulates components that use a global function like $t for i18n -->
<!-- this function can be mocked using global.mocks -->
<div>{{ $t('world') }}</div>
</template>
17 changes: 17 additions & 0 deletions tests/components/HelloFromVitestPlayground.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
const props = defineProps<{ count: number }>()
const times = ref(2)
const result = computed(() => props.count * times.value)
defineExpose(props)
</script>

<template>
<div>{{ count }} x {{ times }} = {{ result }}</div>
<button @click="times += 1">
x1
</button>
</template>
11 changes: 11 additions & 0 deletions tests/components/ScriptSetupWithI18n.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script setup lang="ts">
import { ref } from 'vue'
const hello = ref('hello')
</script>

<template>
<div>{{ hello }}</div>
<!-- this emulates components that use a global function like $t for i18n -->
<!-- this function can be mocked using global.mocks -->
<div>{{ $t('world') }}</div>
</template>
11 changes: 10 additions & 1 deletion tests/mount.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import { defineComponent } from 'vue'
import { mount } from '../src'
import HelloFromVitestPlayground from './components/HelloFromVitestPlayground.vue'

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

expect(wrapper.html()).toBe('<div>hello</div>')
})

it('should not warn on readonly hasOwnProperty when mounting a component', () => {
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {})

mount(HelloFromVitestPlayground, { props: { count: 2 } })

expect(spy).not.toHaveBeenCalled()
})
})
26 changes: 26 additions & 0 deletions tests/mountingOptions/mocks.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { describe, expect, it, vi } from 'vitest'
import { mount, RouterLinkStub } from '../../src'
import { defineComponent } from 'vue'
import ScriptSetupWithI18n from '../components/ScriptSetupWithI18n.vue'
import ComponentWithI18n from '../components/ComponentWithI18n.vue'

describe('mocks', () => {
it('mocks a vuex store', async () => {
Expand Down Expand Up @@ -75,4 +77,28 @@ describe('mocks', () => {
await wrapper.find('button').trigger('click')
expect($router.push).toHaveBeenCalledWith('/posts/1')
})

it('mocks a global function in a script setup component', () => {
const wrapper = mount(ScriptSetupWithI18n, {
global: {
mocks: {
$t: () => 'mocked'
}
}
})
expect(wrapper.text()).toContain('hello')
expect(wrapper.text()).toContain('mocked')
})

it('mocks a global function in an option component', () => {
const wrapper = mount(ComponentWithI18n, {
global: {
mocks: {
$t: () => 'mocked'
}
}
})
expect(wrapper.text()).toContain('hello')
expect(wrapper.text()).toContain('mocked')
})
})
1 change: 1 addition & 0 deletions types/testing.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ import type { Router } from 'vue-router'
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$router: Router
$t: (key: string) => string
}
}

0 comments on commit f2b9091

Please sign in to comment.