Skip to content

Commit 3c035d4

Browse files
authored
Merge pull request #82 from vuejs/feature/pluggable-poc
feat: Plugin interface with wrapper in closure
2 parents 41c00e5 + 407a757 commit 3c035d4

File tree

6 files changed

+135
-5
lines changed

6 files changed

+135
-5
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1+
.idea
12
node_modules
23
yarn-error.log
34
dist
4-
coverage
5+
coverage

src/config.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,51 @@
11
import { GlobalMountOptions } from './types'
22

3-
export const config: { global: GlobalMountOptions } = {
4-
global: {}
3+
interface GlobalConfigOptions {
4+
global: GlobalMountOptions
5+
plugins: {
6+
VueWrapper: Pluggable
7+
DOMWrapper: Pluggable
8+
}
9+
}
10+
11+
class Pluggable {
12+
installedPlugins: any
13+
constructor() {
14+
this.installedPlugins = []
15+
}
16+
17+
install(handler, options = {}) {
18+
if (typeof handler !== 'function') {
19+
console.error('plugin.install must receive a function')
20+
handler = () => ({})
21+
}
22+
this.installedPlugins.push({ handler, options })
23+
}
24+
25+
extend(instance) {
26+
const invokeSetup = (plugin) => plugin.handler(instance) // invoke the setup method passed to install
27+
const bindProperty = ([property, value]: [string, any]) => {
28+
instance[property] =
29+
typeof value === 'function' ? value.bind(instance) : value
30+
}
31+
const addAllPropertiesFromSetup = (setupResult) => {
32+
setupResult = typeof setupResult === 'object' ? setupResult : {}
33+
Object.entries(setupResult).forEach(bindProperty)
34+
}
35+
36+
this.installedPlugins.map(invokeSetup).forEach(addAllPropertiesFromSetup)
37+
}
38+
39+
/** For testing */
40+
reset() {
41+
this.installedPlugins = []
42+
}
43+
}
44+
45+
export const config: GlobalConfigOptions = {
46+
global: {},
47+
plugins: {
48+
VueWrapper: new Pluggable(),
49+
DOMWrapper: new Pluggable()
50+
}
551
}

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { mount } from './mount'
22
import { RouterLinkStub } from './components/RouterLinkStub'
3+
import { VueWrapper } from './vue-wrapper'
34
import { config } from './config'
45

5-
export { mount, RouterLinkStub, config }
6+
export { mount, RouterLinkStub, VueWrapper, config }

src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ interface NameSelector {
2727
name: string
2828
}
2929

30+
interface RefSelector {
31+
ref: string
32+
}
33+
34+
interface NameSelector {
35+
name: string
36+
}
37+
3038
export type FindComponentSelector = RefSelector | NameSelector | string
3139
export type FindAllComponentsSelector = NameSelector | string
3240

src/vue-wrapper.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { ComponentPublicInstance, nextTick, App, render } from 'vue'
1+
import { ComponentPublicInstance, nextTick, App } from 'vue'
22
import { ShapeFlags } from '@vue/shared'
3+
import { config } from './config'
34

45
import { DOMWrapper } from './dom-wrapper'
56
import {
@@ -11,6 +12,7 @@ import { ErrorWrapper } from './error-wrapper'
1112
import { TriggerOptions } from './create-dom-event'
1213
import { find } from './utils/find'
1314

15+
// @ts-ignore
1416
export class VueWrapper<T extends ComponentPublicInstance>
1517
implements WrapperAPI {
1618
private componentVM: T
@@ -27,6 +29,8 @@ export class VueWrapper<T extends ComponentPublicInstance>
2729
this.rootVM = vm.$root
2830
this.componentVM = vm as T
2931
this.__setProps = setProps
32+
// plugins hook
33+
config.plugins.VueWrapper.extend(this)
3034
}
3135

3236
private get hasMultipleRoots(): boolean {

tests/features/plugins.spec.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { ComponentPublicInstance } from 'vue'
2+
3+
import { mount, config } from '../../src'
4+
import { WrapperAPI } from '../../src/types'
5+
6+
declare module '../../src/vue-wrapper' {
7+
interface VueWrapper<T extends ComponentPublicInstance> {
8+
width(): number
9+
$el: Element
10+
myMethod(): void
11+
}
12+
}
13+
14+
const textValue = `I'm the innerHTML`
15+
const mountComponent = () => mount({ template: `<h1>${textValue}</h1>` })
16+
17+
describe('Plugin', () => {
18+
describe('#install method', () => {
19+
beforeEach(() => {
20+
config.plugins.VueWrapper.reset()
21+
})
22+
23+
it('extends wrappers with the return values from the install function', () => {
24+
const width = 230
25+
const plugin = () => ({ width })
26+
config.plugins.VueWrapper.install(plugin)
27+
const wrapper = mountComponent()
28+
expect(wrapper).toHaveProperty('width', width)
29+
})
30+
31+
it('receives the wrapper inside the plugin setup', () => {
32+
const plugin = (wrapper: WrapperAPI) => {
33+
return {
34+
$el: wrapper.element // simple aliases
35+
}
36+
}
37+
config.plugins.VueWrapper.install(plugin)
38+
const wrapper = mountComponent()
39+
expect(wrapper.$el.innerHTML).toEqual(textValue)
40+
})
41+
42+
it('supports functions', () => {
43+
const myMethod = jest.fn()
44+
const plugin = () => ({ myMethod })
45+
config.plugins.VueWrapper.install(plugin)
46+
mountComponent().myMethod()
47+
expect(myMethod).toHaveBeenCalledTimes(1)
48+
})
49+
50+
describe('error states', () => {
51+
const plugins = [
52+
() => false,
53+
() => true,
54+
() => [],
55+
true,
56+
false,
57+
'property',
58+
120
59+
]
60+
61+
it.each(plugins)(
62+
'Calling install with %p is handled gracefully',
63+
(plugin) => {
64+
config.plugins.VueWrapper.install(plugin)
65+
expect(() => mountComponent()).not.toThrow()
66+
}
67+
)
68+
})
69+
})
70+
})

0 commit comments

Comments
 (0)