Skip to content

Commit 958b91b

Browse files
authored
Merge pull request #232 from vuejs/issue-158-functional-emits
Issue 158 functional emits
2 parents 6dc4c64 + 33a6522 commit 958b91b

File tree

8 files changed

+725
-516
lines changed

8 files changed

+725
-516
lines changed

package.json

+8-8
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,27 @@
2121
"@rollup/plugin-node-resolve": "^7.1.3",
2222
"@rollup/plugin-replace": "^2.3.2",
2323
"@types/estree": "^0.0.42",
24-
"@types/jest": "^24.9.1",
24+
"@types/jest": "25.2.1",
2525
"@types/node": "12.12.35",
26-
"@vue/compiler-sfc": "^3.0.1",
26+
"@vue/compiler-sfc": "^3.0.2",
2727
"babel-jest": "^25.2.3",
2828
"babel-preset-jest": "^25.2.1",
2929
"dom-event-types": "^1.0.0",
3030
"husky": "^4.2.3",
31-
"jest": "^25.1.0",
31+
"jest": "25.2.1",
3232
"jsdom": "^16.2.2",
3333
"jsdom-global": "^3.0.2",
3434
"lint-staged": "^10.0.9",
3535
"prettier": "^2.0.2",
3636
"rollup": "^1.31.1",
3737
"rollup-plugin-typescript2": "^0.26.0",
38-
"ts-jest": "^25.0.0",
38+
"ts-jest": "25.2.1",
3939
"tsd": "0.11.0",
4040
"typescript": "^3.7.5",
41-
"vue": "^3.0.1",
42-
"vue-jest": "vuejs/vue-jest#next",
43-
"vue-router": "^4.0.0-alpha.14",
44-
"vuex": "^4.0.0-beta.1"
41+
"vue": "^3.0.2",
42+
"vue-jest": "^5.0.0-alpha.5",
43+
"vue-router": "^4.0.0-rc.1",
44+
"vuex": "^4.0.0-beta.4"
4545
},
4646
"peerDependencies": {
4747
"vue": "^3.0.1"

src/mount.ts

+24-23
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
VNode,
55
defineComponent,
66
VNodeNormalizedChildren,
7-
transformVNodeArgs,
87
reactive,
98
FunctionalComponent,
109
ComponentPublicInstance,
@@ -27,7 +26,7 @@ import {
2726

2827
import { config } from './config'
2928
import { GlobalMountOptions } from './types'
30-
import { mergeGlobalProperties, isFunctionalComponent } from './utils'
29+
import { mergeGlobalProperties } from './utils'
3130
import { processSlot } from './utils/compileSlots'
3231
import { createWrapper, VueWrapper } from './vueWrapper'
3332
import { attachEmitListener } from './emitMixin'
@@ -68,12 +67,9 @@ export type ObjectEmitsOptions = Record<
6867
>
6968
export type EmitsOptions = ObjectEmitsOptions | string[]
7069

71-
// Functional component
72-
export function mount<
73-
TestedComponent extends FunctionalComponent<Props>,
74-
Props
75-
>(
76-
originalComponent: TestedComponent,
70+
// Functional component with emits
71+
export function mount<Props, E extends EmitsOptions = {}>(
72+
originalComponent: FunctionalComponent<Props, E>,
7773
options?: MountingOptions<Props>
7874
): VueWrapper<ComponentPublicInstance<Props>>
7975

@@ -228,13 +224,25 @@ export function mount(
228224
options?: MountingOptions<any>
229225
): VueWrapper<any> {
230226
// normalise the incoming component
231-
const component =
232-
typeof originalComponent === 'function'
233-
? defineComponent({
234-
setup: (_, { attrs, slots }) => () =>
235-
h(originalComponent, attrs, slots)
236-
})
237-
: { ...originalComponent }
227+
let component
228+
229+
const functionalComponentEmits: Record<string, unknown[]> = {}
230+
231+
if (typeof originalComponent === 'function') {
232+
// we need to wrap it like this so we can capture emitted events.
233+
// we capture events using a mixin that mutates `emit` in `beforeCreate`,
234+
// but functional components do not support mixins, so we need to wrap it
235+
// and make it a non-functional component for testing purposes.
236+
component = defineComponent({
237+
setup: (_, { attrs, slots, emit }) => () => {
238+
return h((props: any, ctx: any) =>
239+
originalComponent(props, { ...ctx, ...attrs, emit, slots })
240+
)
241+
}
242+
})
243+
} else {
244+
component = { ...originalComponent }
245+
}
238246

239247
const el = document.createElement('div')
240248

@@ -400,14 +408,7 @@ export function mount(
400408
const vm = app.mount(el)
401409

402410
const App = vm.$refs[MOUNT_COMPONENT_REF] as ComponentPublicInstance
403-
return createWrapper(
404-
app,
405-
App,
406-
{
407-
isFunctionalComponent: isFunctionalComponent(originalComponent)
408-
},
409-
setProps
410-
)
411+
return createWrapper(app, App, setProps, functionalComponentEmits)
411412
}
412413

413414
export const shallowMount: typeof mount = (component: any, options?: any) => {

src/types.ts

-4
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,3 @@ export type GlobalMountOptions = {
3030
stubs?: Record<any, any>
3131
renderStubDefaultSlot?: boolean
3232
}
33-
34-
export interface VueWrapperMeta {
35-
isFunctionalComponent: boolean
36-
}

src/utils.ts

-4
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,6 @@ export function mergeGlobalProperties(
3737
}
3838
}
3939

40-
export function isFunctionalComponent(component: any) {
41-
return typeof component === 'function'
42-
}
43-
4440
// https://stackoverflow.com/a/48218209
4541
export const mergeDeep = (
4642
target: Record<string, any>,

src/vueWrapper.ts

+12-32
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,30 @@ import { ShapeFlags } from '@vue/shared'
33

44
import { config } from './config'
55
import { DOMWrapper } from './domWrapper'
6-
import {
7-
FindAllComponentsSelector,
8-
FindComponentSelector,
9-
VueWrapperMeta
10-
} from './types'
6+
import { FindAllComponentsSelector, FindComponentSelector } from './types'
117
import { createWrapperError } from './errorWrapper'
128
import { TriggerOptions } from './createDomEvent'
139
import { find, matches } from './utils/find'
14-
import { isFunctionalComponent, mergeDeep } from './utils'
10+
import { mergeDeep } from './utils'
1511

1612
export class VueWrapper<T extends ComponentPublicInstance> {
1713
private componentVM: T
1814
private rootVM: ComponentPublicInstance
1915
private __app: App | null
2016
private __setProps: ((props: Record<string, any>) => void) | undefined
21-
private __isFunctionalComponent: boolean
17+
private __functionalEmits: Record<string, unknown[]>
2218

2319
constructor(
2420
app: App | null,
2521
vm: ComponentPublicInstance,
2622
setProps?: (props: Record<string, any>) => void,
27-
meta?: VueWrapperMeta
23+
functionalEmits?: Record<string, unknown[]>
2824
) {
2925
this.__app = app
3026
this.rootVM = vm.$root!
3127
this.componentVM = vm as T
3228
this.__setProps = setProps
33-
this.__isFunctionalComponent = meta.isFunctionalComponent
29+
this.__functionalEmits = functionalEmits
3430
// plugins hook
3531
config.plugins.VueWrapper.extend(this)
3632
}
@@ -79,12 +75,6 @@ export class VueWrapper<T extends ComponentPublicInstance> {
7975
emitted<T = unknown>(): Record<string, T[]>
8076
emitted<T = unknown>(eventName?: string): T[]
8177
emitted<T = unknown>(eventName?: string): T[] | Record<string, T[]> {
82-
if (this.__isFunctionalComponent) {
83-
console.warn(
84-
'[Vue Test Utils]: capture events emitted from functional components is currently not supported.'
85-
)
86-
}
87-
8878
if (eventName) {
8979
const emitted = (this.vm['__emitted'] as Record<string, T[]>)[eventName]
9080
return emitted
@@ -151,27 +141,21 @@ export class VueWrapper<T extends ComponentPublicInstance> {
151141
if (typeof selector === 'object' && 'ref' in selector) {
152142
const result = this.vm.$refs[selector.ref]
153143
if (result) {
154-
return createWrapper(null, result as T, {
155-
isFunctionalComponent: isFunctionalComponent(result)
156-
})
144+
return createWrapper(null, result as T)
157145
}
158146
}
159147

160148
const result = find(this.vm.$.subTree, selector)
161149
if (result.length) {
162-
return createWrapper(null, result[0], {
163-
isFunctionalComponent: isFunctionalComponent(result)
164-
})
150+
return createWrapper(null, result[0])
165151
}
166152

167153
// https://github.com/vuejs/vue-test-utils-next/issues/211
168154
// VTU v1 supported finding the component mounted itself.
169155
// eg: mount(Comp).findComponent(Comp)
170156
// this is the same as doing `wrapper.vm`, but we keep this behavior for back compat.
171157
if (matches(this.vm.$.vnode, selector)) {
172-
return createWrapper(null, this.vm.$.vnode.component.proxy, {
173-
isFunctionalComponent: false
174-
})
158+
return createWrapper(null, this.vm.$.vnode.component.proxy)
175159
}
176160

177161
return createWrapperError('VueWrapper')
@@ -207,11 +191,7 @@ export class VueWrapper<T extends ComponentPublicInstance> {
207191
}
208192

209193
findAllComponents(selector: FindAllComponentsSelector): VueWrapper<T>[] {
210-
return find(this.vm.$.subTree, selector).map((c) =>
211-
createWrapper(null, c, {
212-
isFunctionalComponent: isFunctionalComponent(c)
213-
})
214-
)
194+
return find(this.vm.$.subTree, selector).map((c) => createWrapper(null, c))
215195
}
216196

217197
findAll<K extends keyof HTMLElementTagNameMap>(
@@ -266,8 +246,8 @@ export class VueWrapper<T extends ComponentPublicInstance> {
266246
export function createWrapper<T extends ComponentPublicInstance>(
267247
app: App | null,
268248
vm: ComponentPublicInstance,
269-
meta: VueWrapperMeta,
270-
setProps?: (props: Record<string, any>) => void
249+
setProps?: (props: Record<string, any>) => void,
250+
functionalComponentEmits?: Record<string, unknown[]>
271251
): VueWrapper<T> {
272-
return new VueWrapper<T>(app, vm, setProps, meta)
252+
return new VueWrapper<T>(app, vm, setProps, functionalComponentEmits)
273253
}

test-dts/mount.d-test.ts

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { expectError, expectType } from 'tsd'
2-
import { DefineComponent, defineComponent, reactive } from 'vue'
2+
import {
3+
DefineComponent,
4+
defineComponent,
5+
FunctionalComponent,
6+
reactive
7+
} from 'vue'
38
import { mount } from '../src'
49

510
const AppWithDefine = defineComponent({
@@ -177,3 +182,24 @@ mount(ShimComponent, {
177182
}
178183
}
179184
})
185+
186+
// functional components
187+
declare const FunctionalComponent: FunctionalComponent<{
188+
bar: string
189+
level: number
190+
}>
191+
declare const FunctionalComponentEmit: FunctionalComponent<
192+
{
193+
bar: string
194+
level: number
195+
},
196+
{ hello: (foo: string, bar: string) => void }
197+
>
198+
199+
mount(FunctionalComponent)
200+
mount(defineComponent(FunctionalComponent))
201+
202+
mount(FunctionalComponentEmit)
203+
204+
// @ts-ignore vue 3.0.2 doesn't work. FIX: https://github.com/vuejs/vue-next/pull/2494
205+
mount(defineComponent(FunctionalComponentEmit))

tests/emit.spec.ts

+17-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defineComponent, h } from 'vue'
1+
import { defineComponent, FunctionalComponent, h, SetupContext } from 'vue'
22

33
import { mount } from '../src'
44

@@ -134,14 +134,24 @@ describe('emitted', () => {
134134
})
135135

136136
it('gives a useful warning for functional components', () => {
137-
const Component = (_, ctx) => {
138-
return h('button', { onClick: () => ctx.emit('hello', 'foo', 'bar') })
137+
const Component: FunctionalComponent<
138+
{ bar: string; level: number },
139+
{ hello: (foo: string, bar: string) => void }
140+
> = (props, ctx) => {
141+
return h(`h${props.level}`, {
142+
onClick: () => ctx.emit('hello', 'foo', props.bar)
143+
})
139144
}
140145

141-
mount(Component).emitted()
146+
const wrapper = mount(Component, {
147+
props: {
148+
bar: 'bar',
149+
level: 1
150+
}
151+
})
142152

143-
expect(console.warn).toHaveBeenCalledWith(
144-
'[Vue Test Utils]: capture events emitted from functional components is currently not supported.'
145-
)
153+
wrapper.find('h1').trigger('click')
154+
expect(wrapper.emitted('hello')).toHaveLength(1)
155+
expect(wrapper.emitted('hello')[0]).toEqual(['foo', 'bar'])
146156
})
147157
})

0 commit comments

Comments
 (0)