diff --git a/src/mount.ts b/src/mount.ts index 0bfd45f24..a991814d6 100644 --- a/src/mount.ts +++ b/src/mount.ts @@ -33,6 +33,7 @@ import { MountingOptions, Slot } from './types' import { getComponentsFromStubs, getDirectivesFromStubs, + hasSetupState, isFunctionalComponent, isObject, isObjectComponent, @@ -475,15 +476,23 @@ export function mount( // global mocks mixin if (global?.mocks) { - const mixin = { + const mixin = defineComponent({ beforeCreate() { for (const [k, v] of Object.entries( global.mocks as { [key: string]: any } )) { - ;(this as any)[k] = v + // 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 as any)) { + ;(this as any).$.setupState[k] = v + } else { + ;(this as any)[k] = v + } } } - } + }) app.mixin(mixin) } diff --git a/src/utils.ts b/src/utils.ts index 0a9c49564..e2780518f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,6 +2,7 @@ import { GlobalMountOptions, RefSelector, Stub, Stubs } from './types' import { Component, ComponentOptions, + ComponentPublicInstance, ConcreteComponent, Directive, FunctionalComponent @@ -185,3 +186,11 @@ export function getDirectivesFromStubs( .map(([key, value]) => [key.substring(1), value]) ) as Record } +export function hasSetupState( + vm: ComponentPublicInstance +): vm is ComponentPublicInstance & { setupState: Record } { + return ( + vm && + (vm.$ as unknown as { devtoolsRawSetupState: any }).devtoolsRawSetupState + ) +} diff --git a/src/vueWrapper.ts b/src/vueWrapper.ts index 6995c7d66..356ae09d9 100644 --- a/src/vueWrapper.ts +++ b/src/vueWrapper.ts @@ -8,7 +8,7 @@ import { import { config } from './config' import domEvents from './constants/dom-events' import { VueElement, VueNode } from './types' -import { mergeDeep } from './utils' +import { hasSetupState, mergeDeep } from './utils' import { getRootNodes } from './utils/getRootNodes' import { emitted, recordEvent, removeEventHistory } from './emit' import BaseWrapper from './baseWrapper' @@ -107,10 +107,7 @@ export class VueWrapper< // This does not work for functional components though (as they have no vm) // or for components with a setup that returns a render function (as they have an empty proxy) // in both cases, we return `vm` directly instead - if ( - vm && - (vm.$ as unknown as { devtoolsRawSetupState: any }).devtoolsRawSetupState - ) { + if (hasSetupState(vm)) { this.componentVM = createVMProxy(vm, (vm.$ as any).setupState) } else { this.componentVM = vm diff --git a/tests/expose.spec.ts b/tests/expose.spec.ts index 16364149d..357d4000b 100644 --- a/tests/expose.spec.ts +++ b/tests/expose.spec.ts @@ -91,4 +91,23 @@ describe('expose', () => { await wrapper.find('button').trigger('click') expect(wrapper.html()).toContain('3') }) + + it('should not throw when mocking', async () => { + const spiedIncrement = vi.fn() + const wrapper = mount(ScriptSetup, { + global: { + mocks: { + count: -1, + inc: spiedIncrement + } + } + }) + expect(wrapper.html()).toContain('-1') + + await wrapper.find('button').trigger('click') + await nextTick() + + expect(spiedIncrement).toHaveBeenCalled() + expect(wrapper.html()).toContain('-1') + }) })