diff --git a/src/mount.ts b/src/mount.ts index 8a607aa47..27735767c 100644 --- a/src/mount.ts +++ b/src/mount.ts @@ -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 } } @@ -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) diff --git a/src/utils.ts b/src/utils.ts index a6a99185b..be0cad37b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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] diff --git a/tests/components/ComponentWithI18n.vue b/tests/components/ComponentWithI18n.vue new file mode 100644 index 000000000..d955296a3 --- /dev/null +++ b/tests/components/ComponentWithI18n.vue @@ -0,0 +1,16 @@ + + + diff --git a/tests/components/HelloFromVitestPlayground.vue b/tests/components/HelloFromVitestPlayground.vue new file mode 100644 index 000000000..d1bf7f677 --- /dev/null +++ b/tests/components/HelloFromVitestPlayground.vue @@ -0,0 +1,17 @@ + + + diff --git a/tests/components/ScriptSetupWithI18n.vue b/tests/components/ScriptSetupWithI18n.vue new file mode 100644 index 000000000..2540d36c2 --- /dev/null +++ b/tests/components/ScriptSetupWithI18n.vue @@ -0,0 +1,11 @@ + + + diff --git a/tests/mount.spec.ts b/tests/mount.spec.ts index 74f5b1c10..7c77d66c4 100644 --- a/tests/mount.spec.ts +++ b/tests/mount.spec.ts @@ -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', () => { @@ -23,4 +24,12 @@ describe('mount: general tests', () => { expect(wrapper.html()).toBe('
hello
') }) + + 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() + }) }) diff --git a/tests/mountingOptions/mocks.spec.ts b/tests/mountingOptions/mocks.spec.ts index a37412814..2d9d410b1 100644 --- a/tests/mountingOptions/mocks.spec.ts +++ b/tests/mountingOptions/mocks.spec.ts @@ -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 () => { @@ -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') + }) }) diff --git a/types/testing.d.ts b/types/testing.d.ts index 4aa77f8f2..e67dcaac7 100644 --- a/types/testing.d.ts +++ b/types/testing.d.ts @@ -3,5 +3,6 @@ import type { Router } from 'vue-router' declare module '@vue/runtime-core' { interface ComponentCustomProperties { $router: Router + $t: (key: string) => string } }