Skip to content

Commit

Permalink
Merge pull request #82 from vuejs/feature/pluggable-poc
Browse files Browse the repository at this point in the history
feat: Plugin interface with wrapper in closure
  • Loading branch information
lmiller1990 authored Apr 24, 2020
2 parents 41c00e5 + 407a757 commit 3c035d4
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 5 deletions.
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 = () => ({})
}
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 }
8 changes: 8 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
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
70 changes: 70 additions & 0 deletions tests/features/plugins.spec.ts
Original file line number Diff line number Diff line change
@@ -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<T extends ComponentPublicInstance> {
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
}
}
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()
}
)
})
})
})

0 comments on commit 3c035d4

Please sign in to comment.