diff --git a/package.json b/package.json index 946ca79f7..ea432180b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vue/test-utils", - "version": "2.2.1", + "version": "2.2.3", "license": "MIT", "main": "dist/vue-test-utils.cjs.js", "unpkg": "dist/vue-test-utils.browser.js", @@ -33,7 +33,7 @@ "@typescript-eslint/parser": "5.42.1", "@vitejs/plugin-vue": "3.2.0", "@vitejs/plugin-vue-jsx": "2.1.1", - "@vitest/coverage-c8": "0.25.1", + "@vitest/coverage-c8": "0.25.2", "@vue/compat": "3.2.45", "@vue/compiler-dom": "3.2.45", "@vue/compiler-sfc": "3.2.45", @@ -55,7 +55,7 @@ "unplugin-vue-components": "0.22.9", "vite": "3.2.3", "vitepress": "0.22.4", - "vitest": "0.25.1", + "vitest": "0.25.2", "vue": "3.2.45", "vue-class-component": "8.0.0-rc.1", "vue-router": "4.1.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25234443c..4557ee62b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,7 +12,7 @@ specifiers: '@typescript-eslint/parser': 5.42.1 '@vitejs/plugin-vue': 3.2.0 '@vitejs/plugin-vue-jsx': 2.1.1 - '@vitest/coverage-c8': 0.25.1 + '@vitest/coverage-c8': 0.25.2 '@vue/compat': 3.2.45 '@vue/compiler-dom': 3.2.45 '@vue/compiler-sfc': 3.2.45 @@ -34,7 +34,7 @@ specifiers: unplugin-vue-components: 0.22.9 vite: 3.2.3 vitepress: 0.22.4 - vitest: 0.25.1 + vitest: 0.25.2 vue: 3.2.45 vue-class-component: 8.0.0-rc.1 vue-router: 4.1.6 @@ -53,7 +53,7 @@ devDependencies: '@typescript-eslint/parser': 5.42.1_rmayb2veg2btbq6mbmnyivgasy '@vitejs/plugin-vue': 3.2.0_vite@3.2.3+vue@3.2.45 '@vitejs/plugin-vue-jsx': 2.1.1_vite@3.2.3+vue@3.2.45 - '@vitest/coverage-c8': 0.25.1_jsdom@20.0.2 + '@vitest/coverage-c8': 0.25.2_jsdom@20.0.2 '@vue/compat': 3.2.45_vue@3.2.45 '@vue/compiler-dom': 3.2.45 '@vue/compiler-sfc': 3.2.45 @@ -75,7 +75,7 @@ devDependencies: unplugin-vue-components: 0.22.9_rollup@3.2.5+vue@3.2.45 vite: 3.2.3_@types+node@18.11.9 vitepress: 0.22.4 - vitest: 0.25.1_jsdom@20.0.2 + vitest: 0.25.2_jsdom@20.0.2 vue: 3.2.45 vue-class-component: 8.0.0-rc.1_vue@3.2.45 vue-router: 4.1.6_vue@3.2.45 @@ -962,11 +962,11 @@ packages: vue: 3.2.45 dev: true - /@vitest/coverage-c8/0.25.1_jsdom@20.0.2: - resolution: {integrity: sha512-gpl5QNaNeIN0mfRiosCqBFoZcizb5GA458TDnOQXkGDc4kklazxn70u9evGfV62wiiAUfGGebgRhxlBkAa6m6g==} + /@vitest/coverage-c8/0.25.2_jsdom@20.0.2: + resolution: {integrity: sha512-qKsiUJh3bjbB5Q229CbxEWCqiDBwvIrcZ9OOuQdMEC0pce3/LlTUK3+K3hd7WqAYEbbiqXfC5MVMKHZkV82PgA==} dependencies: c8: 7.12.0 - vitest: 0.25.1_jsdom@20.0.2 + vitest: 0.25.2_jsdom@20.0.2 transitivePeerDependencies: - '@edge-runtime/vm' - '@vitest/browser' @@ -3714,8 +3714,8 @@ packages: - stylus dev: true - /vitest/0.25.1_jsdom@20.0.2: - resolution: {integrity: sha512-eH74h6MkuEgsqR4mAQZeMK9O0PROiKY+i+1GMz/fBi5A3L2ml5U7JQs7LfPU7+uWUziZyLHagl+rkyfR8SLhlA==} + /vitest/0.25.2_jsdom@20.0.2: + resolution: {integrity: sha512-qqkzfzglEFbQY7IGkgSJkdOhoqHjwAao/OrphnHboeYHC5JzsVFoLCaB2lnAy8krhj7sbrFTVRApzpkTOeuDWQ==} engines: {node: '>=v14.16.0'} hasBin: true peerDependencies: 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 3ecd81263..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' @@ -35,7 +35,9 @@ function createVMProxy( if (key in setupState) { return Reflect.get(setupState, key, receiver) } else { - return (vm as any)[key] + // vm.$.ctx is the internal context of the vm + // with all variables, methods and props + return (vm as any).$.ctx[key] } }, set(vm, key, value, receiver) { @@ -45,12 +47,29 @@ function createVMProxy( return Reflect.set(vm, key, value, receiver) } }, + has(vm, property) { + return Reflect.has(setupState, property) || Reflect.has(vm, property) + }, + defineProperty(vm, key, attributes) { + if (key in setupState) { + return Reflect.defineProperty(setupState, key, attributes) + } else { + return Reflect.defineProperty(vm, key, attributes) + } + }, getOwnPropertyDescriptor(vm, property) { if (property in setupState) { return Reflect.getOwnPropertyDescriptor(setupState, property) } else { return Reflect.getOwnPropertyDescriptor(vm, property) } + }, + deleteProperty(vm, property) { + if (property in setupState) { + return Reflect.deleteProperty(setupState, property) + } else { + return Reflect.deleteProperty(vm, property) + } } }) } @@ -88,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/components/ScriptSetup.vue b/tests/components/ScriptSetup.vue index d79d16ab5..48402e806 100644 --- a/tests/components/ScriptSetup.vue +++ b/tests/components/ScriptSetup.vue @@ -12,6 +12,6 @@ const inc = () => { diff --git a/tests/components/ScriptSetupWithProps.vue b/tests/components/ScriptSetupWithProps.vue new file mode 100644 index 000000000..8a2b1c09f --- /dev/null +++ b/tests/components/ScriptSetupWithProps.vue @@ -0,0 +1,13 @@ + + + diff --git a/tests/expose.spec.ts b/tests/expose.spec.ts index 2b09247bf..357d4000b 100644 --- a/tests/expose.spec.ts +++ b/tests/expose.spec.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { nextTick } from 'vue' import { mount } from '../src' import Hello from './components/Hello.vue' @@ -6,6 +6,7 @@ import DefineExpose from './components/DefineExpose.vue' import DefineExposeWithRenderFunction from './components/DefineExposeWithRenderFunction.vue' import ScriptSetupExpose from './components/ScriptSetup_Expose.vue' import ScriptSetup from './components/ScriptSetup.vue' +import ScriptSetupWithProps from './components/ScriptSetupWithProps.vue' describe('expose', () => { it('access vm on simple components', async () => { @@ -61,4 +62,52 @@ describe('expose', () => { await nextTick() expect(wrapper.html()).toContain('2') }) + + it('spies on vm with