Skip to content
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

POC: Plugin interface with wrapper in closure #82

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
40 changes: 40 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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 = {
plugins: {
VueWrapper: new Pluggable(),
DOMWrapper: new Pluggable()
}
}
5 changes: 4 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { mount } from './mount'
import { RouterLinkStub } from './components/RouterLinkStub'
import { VueWrapper } from './vue-wrapper'

export { mount, RouterLinkStub }
import { config } from './config'

export { mount, RouterLinkStub, VueWrapper, config }
13 changes: 13 additions & 0 deletions src/vue-wrapper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ComponentPublicInstance, nextTick } from 'vue'
import { ShapeFlags } from '@vue/shared'
import { config } from './config'

import { DOMWrapper } from './dom-wrapper'
import { WrapperAPI } from './types'
Expand All @@ -21,6 +22,9 @@ export class VueWrapper implements WrapperAPI {
this.__setProps = setProps
this.componentVM = this.vm.$refs['VTU_COMPONENT'] as ComponentPublicInstance
this.__emitted = events

// plugins hook
config.plugins.VueWrapper.extend(this)
}

private get appRootNode() {
Expand Down Expand Up @@ -93,6 +97,15 @@ export class VueWrapper implements WrapperAPI {
const rootElementWrapper = new DOMWrapper(this.element)
return rootElementWrapper.trigger(eventString)
}

extend() {
// plugins hook
Object.entries(config.plugins.VueWrapper).forEach(
([name, handler]: [string, () => any]) => {
this[name] = handler.bind(this)
}
)
}
}

export function createWrapper(
Expand Down
58 changes: 58 additions & 0 deletions tests/features/plugins.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { mount, VueWrapper, config } from '../../src'

declare module '../../src/vue-wrapper' {
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
}
}

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) => {
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()
expect(wrapper.$el.innerHTML).toEqual(textValue)
})

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()
}
)
})
})
})