diff --git a/.gitignore b/.gitignore index c03a38b6e..87c63fdf7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ +.idea node_modules yarn-error.log dist -coverage \ No newline at end of file +coverage diff --git a/src/config.ts b/src/config.ts index 5d20463d5..54c0ab8f7 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,5 +1,51 @@ import { GlobalMountOptions } from './types' -export const config: { global: GlobalMountOptions } = { - global: {} +interface GlobalConfigOptions { + global: GlobalMountOptions + plugins: { + VueWrapper: Pluggable + DOMWrapper: Pluggable + } +} + +class Pluggable { + installedPlugins: any + constructor() { + this.installedPlugins = [] + } + + install(handler, options = {}) { + if (typeof handler !== 'function') { + console.error('plugin.install must receive a function') + handler = () => ({}) + } + this.installedPlugins.push({ handler, options }) + } + + extend(instance) { + const invokeSetup = (plugin) => plugin.handler(instance) // invoke the setup method passed to install + const bindProperty = ([property, value]: [string, any]) => { + instance[property] = + typeof value === 'function' ? value.bind(instance) : value + } + const addAllPropertiesFromSetup = (setupResult) => { + setupResult = typeof setupResult === 'object' ? setupResult : {} + Object.entries(setupResult).forEach(bindProperty) + } + + this.installedPlugins.map(invokeSetup).forEach(addAllPropertiesFromSetup) + } + + /** For testing */ + reset() { + this.installedPlugins = [] + } +} + +export const config: GlobalConfigOptions = { + global: {}, + plugins: { + VueWrapper: new Pluggable(), + DOMWrapper: new Pluggable() + } } diff --git a/src/index.ts b/src/index.ts index 977786aa7..1fbe0d33c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import { mount } from './mount' import { RouterLinkStub } from './components/RouterLinkStub' +import { VueWrapper } from './vue-wrapper' import { config } from './config' -export { mount, RouterLinkStub, config } +export { mount, RouterLinkStub, VueWrapper, config } diff --git a/src/types.ts b/src/types.ts index 1d2965a82..ad1952cd7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -27,6 +27,14 @@ interface NameSelector { name: string } +interface RefSelector { + ref: string +} + +interface NameSelector { + name: string +} + export type FindComponentSelector = RefSelector | NameSelector | string export type FindAllComponentsSelector = NameSelector | string diff --git a/src/vue-wrapper.ts b/src/vue-wrapper.ts index 989d558e5..c0ebe967b 100644 --- a/src/vue-wrapper.ts +++ b/src/vue-wrapper.ts @@ -1,5 +1,6 @@ -import { ComponentPublicInstance, nextTick, App, render } from 'vue' +import { ComponentPublicInstance, nextTick, App } from 'vue' import { ShapeFlags } from '@vue/shared' +import { config } from './config' import { DOMWrapper } from './dom-wrapper' import { @@ -11,6 +12,7 @@ import { ErrorWrapper } from './error-wrapper' import { TriggerOptions } from './create-dom-event' import { find } from './utils/find' +// @ts-ignore export class VueWrapper implements WrapperAPI { private componentVM: T @@ -27,6 +29,8 @@ export class VueWrapper this.rootVM = vm.$root this.componentVM = vm as T this.__setProps = setProps + // plugins hook + config.plugins.VueWrapper.extend(this) } private get hasMultipleRoots(): boolean { diff --git a/tests/features/plugins.spec.ts b/tests/features/plugins.spec.ts new file mode 100644 index 000000000..d8fd333b3 --- /dev/null +++ b/tests/features/plugins.spec.ts @@ -0,0 +1,70 @@ +import { ComponentPublicInstance } from 'vue' + +import { mount, config } from '../../src' +import { WrapperAPI } from '../../src/types' + +declare module '../../src/vue-wrapper' { + interface VueWrapper { + width(): number + $el: Element + myMethod(): void + } +} + +const textValue = `I'm the innerHTML` +const mountComponent = () => mount({ template: `

${textValue}

` }) + +describe('Plugin', () => { + describe('#install method', () => { + beforeEach(() => { + config.plugins.VueWrapper.reset() + }) + + it('extends wrappers with the return values from the install function', () => { + const width = 230 + const plugin = () => ({ width }) + config.plugins.VueWrapper.install(plugin) + const wrapper = mountComponent() + expect(wrapper).toHaveProperty('width', width) + }) + + it('receives the wrapper inside the plugin setup', () => { + const plugin = (wrapper: WrapperAPI) => { + return { + $el: wrapper.element // simple aliases + } + } + config.plugins.VueWrapper.install(plugin) + const wrapper = mountComponent() + expect(wrapper.$el.innerHTML).toEqual(textValue) + }) + + it('supports functions', () => { + const myMethod = jest.fn() + const plugin = () => ({ myMethod }) + config.plugins.VueWrapper.install(plugin) + mountComponent().myMethod() + expect(myMethod).toHaveBeenCalledTimes(1) + }) + + describe('error states', () => { + const plugins = [ + () => false, + () => true, + () => [], + true, + false, + 'property', + 120 + ] + + it.each(plugins)( + 'Calling install with %p is handled gracefully', + (plugin) => { + config.plugins.VueWrapper.install(plugin) + expect(() => mountComponent()).not.toThrow() + } + ) + }) + }) +})