diff --git a/rollup.config.js b/rollup.config.js index d3a65ae96..741fc2206 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -21,7 +21,7 @@ function createEntry(options) { const config = { input, external: [ - 'vue', + 'vue', 'lodash/mergeWith', 'lodash/camelCase', 'lodash/upperFirst', @@ -37,7 +37,7 @@ function createEntry(options) { } if (format === 'es') { - config.output.file = isBrowser ? pkg.browser : pkg.module + config.output.file = isBrowser ? pkg.browser : pkg.module } if (format === 'cjs') { config.output.file = pkg.main diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 000000000..5d20463d5 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,5 @@ +import { GlobalMountOptions } from './types' + +export const config: { global: GlobalMountOptions } = { + global: {} +} diff --git a/src/index.ts b/src/index.ts index 891f92be2..977786aa7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ import { mount } from './mount' import { RouterLinkStub } from './components/RouterLinkStub' +import { config } from './config' -export { mount, RouterLinkStub } +export { mount, RouterLinkStub, config } diff --git a/src/mount.ts b/src/mount.ts index 7dda523b2..0f03bcd40 100644 --- a/src/mount.ts +++ b/src/mount.ts @@ -4,11 +4,7 @@ import { VNode, defineComponent, VNodeNormalizedChildren, - ComponentOptions, transformVNodeArgs, - Plugin, - Directive, - Component, reactive, ComponentPublicInstance, ComponentOptionsWithObjectProps, @@ -17,6 +13,9 @@ import { ExtractPropTypes } from 'vue' +import { config } from './config' +import { GlobalMountOptions } from './types' +import { mergeGlobalProperties } from './utils' import { createWrapper, VueWrapper } from './vue-wrapper' import { attachEmitListener } from './emitMixin' import { createDataMixin } from './dataMixin' @@ -36,17 +35,7 @@ interface MountingOptions { default?: Slot [key: string]: Slot } - global?: { - plugins?: Plugin[] - mixins?: ComponentOptions[] - mocks?: Record - stubs?: Record - provide?: Record - // TODO how to type `defineComponent`? Using `any` for now. - components?: Record - directives?: Record - } - stubs?: Record + global?: GlobalMountOptions } // Component declared with defineComponent @@ -139,11 +128,13 @@ export function mount( // create the app const app = createApp(Parent) + const global = mergeGlobalProperties(config.global, options?.global) + // global mocks mixin - if (options?.global?.mocks) { + if (global?.mocks) { const mixin = { beforeCreate() { - for (const [k, v] of Object.entries(options.global?.mocks)) { + for (const [k, v] of Object.entries(global.mocks)) { this[k] = v } } @@ -153,30 +144,30 @@ export function mount( } // use and plugins from mounting options - if (options?.global?.plugins) { - for (const use of options?.global?.plugins) app.use(use) + if (global?.plugins) { + for (const use of global.plugins) app.use(use) } // use any mixins from mounting options - if (options?.global?.mixins) { - for (const mixin of options?.global?.mixins) app.mixin(mixin) + if (global?.mixins) { + for (const mixin of global.mixins) app.mixin(mixin) } - if (options?.global?.components) { - for (const key of Object.keys(options?.global?.components)) - app.component(key, options.global.components[key]) + if (global?.components) { + for (const key of Object.keys(global.components)) + app.component(key, global.components[key]) } - if (options?.global?.directives) { - for (const key of Object.keys(options?.global?.directives)) - app.directive(key, options.global.directives[key]) + if (global?.directives) { + for (const key of Object.keys(global.directives)) + app.directive(key, global.directives[key]) } // provide any values passed via provides mounting option - if (options?.global?.provide) { - for (const key of Reflect.ownKeys(options.global.provide)) { + if (global?.provide) { + for (const key of Reflect.ownKeys(global.provide)) { // @ts-ignore: https://github.com/microsoft/TypeScript/issues/1863 - app.provide(key, options.global.provide[key]) + app.provide(key, global.provide[key]) } } diff --git a/src/types.ts b/src/types.ts index 184494a67..d49ee8b43 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,5 @@ +import { Component, ComponentOptions, Directive, Plugin } from 'vue' + import { DOMWrapper } from './dom-wrapper' import { ErrorWrapper } from './error-wrapper' @@ -23,3 +25,13 @@ interface NameSelector { export type FindComponentSelector = RefSelector | NameSelector | string export type FindAllComponentsSelector = NameSelector | string + +export type GlobalMountOptions = { + plugins?: Plugin[] + mixins?: ComponentOptions[] + mocks?: Record + provide?: Record + components?: Record + directives?: Record + stubs?: Record +} diff --git a/src/utils.ts b/src/utils.ts index b4a362926..1f7723442 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,7 +2,28 @@ import camelCase from 'lodash/camelCase' import upperFirst from 'lodash/upperFirst' import kebabCase from 'lodash/kebabCase' import flow from 'lodash/flow' +import mergeWith from 'lodash/mergeWith' +import { GlobalMountOptions } from './types' const pascalCase = flow(camelCase, upperFirst) export { kebabCase, pascalCase } + +export function mergeGlobalProperties( + configGlobal = {}, + mountGlobal = {} +): GlobalMountOptions { + return mergeWith({}, configGlobal, mountGlobal, (objValue, srcValue, key) => { + switch (key) { + case 'mocks': + case 'provide': + case 'components': + case 'directives': + case 'globalProperties': + return { ...objValue, ...srcValue } + case 'plugins': + case 'mixins': + return [...(objValue || []), ...(srcValue || [])].filter(Boolean) + } + }) +} diff --git a/tests/config.spec.ts b/tests/config.spec.ts new file mode 100644 index 000000000..c1d1fa417 --- /dev/null +++ b/tests/config.spec.ts @@ -0,0 +1,96 @@ +import { config, mount } from '../src' +import Hello from './components/Hello.vue' + +describe('config', () => { + beforeEach(() => { + config.global = { + components: undefined, + directives: undefined, + mixins: undefined, + plugins: undefined, + mocks: undefined, + provide: undefined + } + }) + + describe('components', () => { + const Component = { + template: '
{{ msg }}
', + props: ['msg'] + } + + it('allows setting components globally', () => { + config.global.components = { Hello } + const wrapper1 = mount(Component, { props: { msg: 'Wrapper1' } }) + const wrapper2 = mount(Component, { props: { msg: 'Wrapper2' } }) + expect(wrapper1.text()).toEqual('Wrapper1 Hello world') + expect(wrapper2.text()).toEqual('Wrapper2 Hello world') + }) + + it('allows overwriting globally set component config on a per mount instance', () => { + config.global.components = { Hello } + const HelloLocal = { template: '
Hello Overwritten
' } + const wrapper1 = mount(Component, { props: { msg: 'Wrapper1' } }) + const wrapper2 = mount(Component, { + props: { msg: 'Wrapper2' }, + global: { components: { Hello: HelloLocal } } + }) + expect(wrapper1.text()).toEqual('Wrapper1 Hello world') + expect(wrapper2.text()).toEqual('Wrapper2 Hello Overwritten') + }) + }) + + describe('directives', () => { + const Directive = { + beforeMount(el: Element) { + el.classList.add('DirectiveAdded') + } + } + const Component = { template: '
msg
' } + + it('allows setting directives globally', () => { + config.global.directives = { Directive } + expect(mount(Component).classes()).toContain('DirectiveAdded') + expect(mount(Component).classes()).toContain('DirectiveAdded') + }) + + it('allows overwriting globally set directives', () => { + config.global.directives = { Directive } + const LocalDirective = { + beforeMount(el: Element) { + el.classList.add('LocallyDirectiveAdded') + } + } + + expect(mount(Component).classes()).toContain('DirectiveAdded') + expect( + mount(Component, { + global: { directives: { Directive: LocalDirective } } + }).classes() + ).toContain('LocallyDirectiveAdded') + }) + }) + + describe('mocks', () => { + it('sets mock everywhere', () => { + config.global.mocks = { + foo: 'bar' + } + const Component = { template: '
{{ foo }}
' } + expect(mount(Component).text()).toEqual('bar') + expect(mount(Component).text()).toEqual('bar') + }) + + it('allows overwriting a global mock', () => { + config.global.mocks = { + foo: 'bar' + } + const Component = { template: '
{{ foo }}
' } + + expect(mount(Component).text()).toEqual('bar') + expect( + mount(Component, { global: { mocks: { foo: 'baz' } } }).text() + ).toEqual('baz') + }) + }) +})