diff --git a/src/utils.ts b/src/utils.ts index 2748805c7..68391a950 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -14,6 +14,26 @@ function mergeStubs(target: Record, source: GlobalMountOptions) { } } +// perform 1-level-deep-pseudo-clone merge in order to prevent config leaks +// example: vue-router overwrites globalProperties.$router +function mergeAppConfig( + configGlobalConfig: GlobalMountOptions['config'], + mountGlobalConfig: GlobalMountOptions['config'] +): Required['config'] { + return { + ...configGlobalConfig, + ...mountGlobalConfig, + globalProperties: { + ...configGlobalConfig?.globalProperties, + ...mountGlobalConfig?.globalProperties + }, + compilerOptions: { + ...configGlobalConfig?.compilerOptions, + ...mountGlobalConfig?.compilerOptions + } + } +} + export function mergeGlobalProperties( mountGlobal: GlobalMountOptions = {} ): Required { @@ -34,7 +54,7 @@ export function mergeGlobalProperties( components: { ...configGlobal.components, ...mountGlobal.components }, provide: { ...configGlobal.provide, ...mountGlobal.provide }, mocks: { ...configGlobal.mocks, ...mountGlobal.mocks }, - config: { ...configGlobal.config, ...mountGlobal.config }, + config: mergeAppConfig(configGlobal.config, mountGlobal.config), directives: { ...configGlobal.directives, ...mountGlobal.directives }, renderStubDefaultSlot } diff --git a/tests/config.spec.ts b/tests/config.spec.ts index d292bf151..a18adb27d 100644 --- a/tests/config.spec.ts +++ b/tests/config.spec.ts @@ -1,4 +1,5 @@ import { defineComponent, ComponentPublicInstance, h, inject } from 'vue' +import type { App } from 'vue' import { config, mount } from '../src' import Hello from './components/Hello.vue' import ComponentWithSlots from './components/ComponentWithSlots.vue' @@ -51,6 +52,43 @@ describe('config', () => { }) }) + describe('config integrity', () => { + it('should not leak config when plugins overwrite globalProperties', async () => { + // test with a function because it's not an "easy to clone" primitive type + const globalRouterMock = { push: jest.fn() } + const pluginRouterMock = { push: jest.fn() } + const Component = defineComponent({ template: '
' }) + + class Plugin { + static install(_app: App) { + _app.config.globalProperties.$router = pluginRouterMock + } + } + + config.global.config.globalProperties = { + $router: globalRouterMock + } + + // first with plugin to overwrite globalRouterMock with pluginRouterMock + const wrapper1 = mount(Component, { + global: { + plugins: [Plugin] + } + }) + + // then without plugin to check if the plugin overwrite is gone + const wrapper2 = mount(Component) + + wrapper1.vm.$router.push('/route-1') + wrapper2.vm.$router.push('/route-2') + + expect(pluginRouterMock.push).toHaveBeenCalledTimes(1) + expect(pluginRouterMock.push).toHaveBeenCalledWith('/route-1') + expect(globalRouterMock.push).toHaveBeenCalledTimes(1) + expect(globalRouterMock.push).toHaveBeenCalledWith('/route-2') + }) + }) + describe('renderStubDefaultSlot', () => { it('should override shallow option when set to true', () => { const comp = mount(ComponentWithSlots, {