Skip to content

POC: Plugin interface with wrapper in closure #82

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Apr 24, 2020
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.idea
node_modules
yarn-error.log
dist
coverage
coverage
50 changes: 48 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -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 = () => ({})
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should decide if we're going to exit the process here or quietly swallow the misconfigured plugin

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think failing loudly is usually a good idea

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The louder the better, ideally with "you screwed up and here's where and how to fix it"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. I'm fine with that. I wish I had a plugin name to point people at.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm didn't think of that. 🤔

}
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()
}
}
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -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 }
21 changes: 21 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,24 @@ export type GlobalMountOptions = {
directives?: Record<string, Directive>
stubs?: Record<any, any>
}

interface RefSelector {
ref: string
}

interface NameSelector {
name: string
}

export type FindComponentSelector = RefSelector | NameSelector | string
export type FindAllComponentsSelector = NameSelector | string

export type GlobalMountOptions = {
plugins?: Plugin[]
mixins?: ComponentOptions[]
mocks?: Record<string, any>
provide?: Record<any, any>
components?: Record<string, Component | object>
directives?: Record<string, Directive>
stubs?: Record<any, any>
}
6 changes: 5 additions & 1 deletion src/vue-wrapper.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<T extends ComponentPublicInstance>
implements WrapperAPI {
private componentVM: T
Expand All @@ -27,6 +29,8 @@ export class VueWrapper<T extends ComponentPublicInstance>
this.rootVM = vm.$root
this.componentVM = vm as T
this.__setProps = setProps
// plugins hook
config.plugins.VueWrapper.extend(this)
}

private get hasMultipleRoots(): boolean {
Expand Down
71 changes: 71 additions & 0 deletions tests/features/plugins.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { mount, config } from '../../src'
import { WrapperAPI } from '../../src/types'

declare module '../../src/vue-wrapper' {
// @ts-ignore
interface VueWrapper {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ts-ignore should be removed here: you should just need to have the same signature than VueWrapper, i.e VueWrapper<T extends ComponentPublicInstance> and TS will be happy to merge the declarations.
You should also be able to remove all the other @ts-ignore in the file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 I will give this a try

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To no-one's surprised I changed the types based on @cexbrayat 's recommendation and it worked

width(): number
$el: Element
myMethod(): void
}
}

const textValue = `I'm the innerHTML`
const mountComponent = () => mount({ template: `<h1>${textValue}</h1>` })

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will we have access to private properties on the instance? Like the rootVM?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, your original example will work. You can get access to the ComponentPublicInstance under componentVM which is private.

Copy link
Contributor Author

@JessicaSachs JessicaSachs Apr 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const namePlugin = wrapper => ({ lowerCaseName: wrapper.componentVM.$.type.name })
config.plugins.VueWrapper.install(namePlugin)
const wrapper = mount({ template: `<h1>Hello</h1>`, name: 'My_Component' })
wrapper.lowerCaseName // 'my_component'

}
}
config.plugins.VueWrapper.install(plugin)
const wrapper = mountComponent()
// @ts-ignore
expect(wrapper.$el.innerHTML).toEqual(textValue)
})

it('supports functions', () => {
const myMethod = jest.fn()
const plugin = () => ({ myMethod })
config.plugins.VueWrapper.install(plugin)
// @ts-ignore
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()
}
)
})
})
})