Skip to content

Commit

Permalink
Merge pull request #232 from vuejs/issue-158-functional-emits
Browse files Browse the repository at this point in the history
Issue 158 functional emits
  • Loading branch information
lmiller1990 committed Oct 29, 2020
2 parents 6dc4c64 + 33a6522 commit 958b91b
Show file tree
Hide file tree
Showing 8 changed files with 725 additions and 516 deletions.
16 changes: 8 additions & 8 deletions package.json
Expand Up @@ -21,27 +21,27 @@
"@rollup/plugin-node-resolve": "^7.1.3",
"@rollup/plugin-replace": "^2.3.2",
"@types/estree": "^0.0.42",
"@types/jest": "^24.9.1",
"@types/jest": "25.2.1",
"@types/node": "12.12.35",
"@vue/compiler-sfc": "^3.0.1",
"@vue/compiler-sfc": "^3.0.2",
"babel-jest": "^25.2.3",
"babel-preset-jest": "^25.2.1",
"dom-event-types": "^1.0.0",
"husky": "^4.2.3",
"jest": "^25.1.0",
"jest": "25.2.1",
"jsdom": "^16.2.2",
"jsdom-global": "^3.0.2",
"lint-staged": "^10.0.9",
"prettier": "^2.0.2",
"rollup": "^1.31.1",
"rollup-plugin-typescript2": "^0.26.0",
"ts-jest": "^25.0.0",
"ts-jest": "25.2.1",
"tsd": "0.11.0",
"typescript": "^3.7.5",
"vue": "^3.0.1",
"vue-jest": "vuejs/vue-jest#next",
"vue-router": "^4.0.0-alpha.14",
"vuex": "^4.0.0-beta.1"
"vue": "^3.0.2",
"vue-jest": "^5.0.0-alpha.5",
"vue-router": "^4.0.0-rc.1",
"vuex": "^4.0.0-beta.4"
},
"peerDependencies": {
"vue": "^3.0.1"
Expand Down
47 changes: 24 additions & 23 deletions src/mount.ts
Expand Up @@ -4,7 +4,6 @@ import {
VNode,
defineComponent,
VNodeNormalizedChildren,
transformVNodeArgs,
reactive,
FunctionalComponent,
ComponentPublicInstance,
Expand All @@ -27,7 +26,7 @@ import {

import { config } from './config'
import { GlobalMountOptions } from './types'
import { mergeGlobalProperties, isFunctionalComponent } from './utils'
import { mergeGlobalProperties } from './utils'
import { processSlot } from './utils/compileSlots'
import { createWrapper, VueWrapper } from './vueWrapper'
import { attachEmitListener } from './emitMixin'
Expand Down Expand Up @@ -68,12 +67,9 @@ export type ObjectEmitsOptions = Record<
>
export type EmitsOptions = ObjectEmitsOptions | string[]

// Functional component
export function mount<
TestedComponent extends FunctionalComponent<Props>,
Props
>(
originalComponent: TestedComponent,
// Functional component with emits
export function mount<Props, E extends EmitsOptions = {}>(
originalComponent: FunctionalComponent<Props, E>,
options?: MountingOptions<Props>
): VueWrapper<ComponentPublicInstance<Props>>

Expand Down Expand Up @@ -228,13 +224,25 @@ export function mount(
options?: MountingOptions<any>
): VueWrapper<any> {
// normalise the incoming component
const component =
typeof originalComponent === 'function'
? defineComponent({
setup: (_, { attrs, slots }) => () =>
h(originalComponent, attrs, slots)
})
: { ...originalComponent }
let component

const functionalComponentEmits: Record<string, unknown[]> = {}

if (typeof originalComponent === 'function') {
// we need to wrap it like this so we can capture emitted events.
// we capture events using a mixin that mutates `emit` in `beforeCreate`,
// but functional components do not support mixins, so we need to wrap it
// and make it a non-functional component for testing purposes.
component = defineComponent({
setup: (_, { attrs, slots, emit }) => () => {
return h((props: any, ctx: any) =>
originalComponent(props, { ...ctx, ...attrs, emit, slots })
)
}
})
} else {
component = { ...originalComponent }
}

const el = document.createElement('div')

Expand Down Expand Up @@ -400,14 +408,7 @@ export function mount(
const vm = app.mount(el)

const App = vm.$refs[MOUNT_COMPONENT_REF] as ComponentPublicInstance
return createWrapper(
app,
App,
{
isFunctionalComponent: isFunctionalComponent(originalComponent)
},
setProps
)
return createWrapper(app, App, setProps, functionalComponentEmits)
}

export const shallowMount: typeof mount = (component: any, options?: any) => {
Expand Down
4 changes: 0 additions & 4 deletions src/types.ts
Expand Up @@ -30,7 +30,3 @@ export type GlobalMountOptions = {
stubs?: Record<any, any>
renderStubDefaultSlot?: boolean
}

export interface VueWrapperMeta {
isFunctionalComponent: boolean
}
4 changes: 0 additions & 4 deletions src/utils.ts
Expand Up @@ -37,10 +37,6 @@ export function mergeGlobalProperties(
}
}

export function isFunctionalComponent(component: any) {
return typeof component === 'function'
}

// https://stackoverflow.com/a/48218209
export const mergeDeep = (
target: Record<string, any>,
Expand Down
44 changes: 12 additions & 32 deletions src/vueWrapper.ts
Expand Up @@ -3,34 +3,30 @@ import { ShapeFlags } from '@vue/shared'

import { config } from './config'
import { DOMWrapper } from './domWrapper'
import {
FindAllComponentsSelector,
FindComponentSelector,
VueWrapperMeta
} from './types'
import { FindAllComponentsSelector, FindComponentSelector } from './types'
import { createWrapperError } from './errorWrapper'
import { TriggerOptions } from './createDomEvent'
import { find, matches } from './utils/find'
import { isFunctionalComponent, mergeDeep } from './utils'
import { mergeDeep } from './utils'

export class VueWrapper<T extends ComponentPublicInstance> {
private componentVM: T
private rootVM: ComponentPublicInstance
private __app: App | null
private __setProps: ((props: Record<string, any>) => void) | undefined
private __isFunctionalComponent: boolean
private __functionalEmits: Record<string, unknown[]>

constructor(
app: App | null,
vm: ComponentPublicInstance,
setProps?: (props: Record<string, any>) => void,
meta?: VueWrapperMeta
functionalEmits?: Record<string, unknown[]>
) {
this.__app = app
this.rootVM = vm.$root!
this.componentVM = vm as T
this.__setProps = setProps
this.__isFunctionalComponent = meta.isFunctionalComponent
this.__functionalEmits = functionalEmits
// plugins hook
config.plugins.VueWrapper.extend(this)
}
Expand Down Expand Up @@ -79,12 +75,6 @@ export class VueWrapper<T extends ComponentPublicInstance> {
emitted<T = unknown>(): Record<string, T[]>
emitted<T = unknown>(eventName?: string): T[]
emitted<T = unknown>(eventName?: string): T[] | Record<string, T[]> {
if (this.__isFunctionalComponent) {
console.warn(
'[Vue Test Utils]: capture events emitted from functional components is currently not supported.'
)
}

if (eventName) {
const emitted = (this.vm['__emitted'] as Record<string, T[]>)[eventName]
return emitted
Expand Down Expand Up @@ -151,27 +141,21 @@ export class VueWrapper<T extends ComponentPublicInstance> {
if (typeof selector === 'object' && 'ref' in selector) {
const result = this.vm.$refs[selector.ref]
if (result) {
return createWrapper(null, result as T, {
isFunctionalComponent: isFunctionalComponent(result)
})
return createWrapper(null, result as T)
}
}

const result = find(this.vm.$.subTree, selector)
if (result.length) {
return createWrapper(null, result[0], {
isFunctionalComponent: isFunctionalComponent(result)
})
return createWrapper(null, result[0])
}

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

return createWrapperError('VueWrapper')
Expand Down Expand Up @@ -207,11 +191,7 @@ export class VueWrapper<T extends ComponentPublicInstance> {
}

findAllComponents(selector: FindAllComponentsSelector): VueWrapper<T>[] {
return find(this.vm.$.subTree, selector).map((c) =>
createWrapper(null, c, {
isFunctionalComponent: isFunctionalComponent(c)
})
)
return find(this.vm.$.subTree, selector).map((c) => createWrapper(null, c))
}

findAll<K extends keyof HTMLElementTagNameMap>(
Expand Down Expand Up @@ -266,8 +246,8 @@ export class VueWrapper<T extends ComponentPublicInstance> {
export function createWrapper<T extends ComponentPublicInstance>(
app: App | null,
vm: ComponentPublicInstance,
meta: VueWrapperMeta,
setProps?: (props: Record<string, any>) => void
setProps?: (props: Record<string, any>) => void,
functionalComponentEmits?: Record<string, unknown[]>
): VueWrapper<T> {
return new VueWrapper<T>(app, vm, setProps, meta)
return new VueWrapper<T>(app, vm, setProps, functionalComponentEmits)
}
28 changes: 27 additions & 1 deletion test-dts/mount.d-test.ts
@@ -1,5 +1,10 @@
import { expectError, expectType } from 'tsd'
import { DefineComponent, defineComponent, reactive } from 'vue'
import {
DefineComponent,
defineComponent,
FunctionalComponent,
reactive
} from 'vue'
import { mount } from '../src'

const AppWithDefine = defineComponent({
Expand Down Expand Up @@ -177,3 +182,24 @@ mount(ShimComponent, {
}
}
})

// functional components
declare const FunctionalComponent: FunctionalComponent<{
bar: string
level: number
}>
declare const FunctionalComponentEmit: FunctionalComponent<
{
bar: string
level: number
},
{ hello: (foo: string, bar: string) => void }
>

mount(FunctionalComponent)
mount(defineComponent(FunctionalComponent))

mount(FunctionalComponentEmit)

// @ts-ignore vue 3.0.2 doesn't work. FIX: https://github.com/vuejs/vue-next/pull/2494
mount(defineComponent(FunctionalComponentEmit))
24 changes: 17 additions & 7 deletions tests/emit.spec.ts
@@ -1,4 +1,4 @@
import { defineComponent, h } from 'vue'
import { defineComponent, FunctionalComponent, h, SetupContext } from 'vue'

import { mount } from '../src'

Expand Down Expand Up @@ -134,14 +134,24 @@ describe('emitted', () => {
})

it('gives a useful warning for functional components', () => {
const Component = (_, ctx) => {
return h('button', { onClick: () => ctx.emit('hello', 'foo', 'bar') })
const Component: FunctionalComponent<
{ bar: string; level: number },
{ hello: (foo: string, bar: string) => void }
> = (props, ctx) => {
return h(`h${props.level}`, {
onClick: () => ctx.emit('hello', 'foo', props.bar)
})
}

mount(Component).emitted()
const wrapper = mount(Component, {
props: {
bar: 'bar',
level: 1
}
})

expect(console.warn).toHaveBeenCalledWith(
'[Vue Test Utils]: capture events emitted from functional components is currently not supported.'
)
wrapper.find('h1').trigger('click')
expect(wrapper.emitted('hello')).toHaveLength(1)
expect(wrapper.emitted('hello')[0]).toEqual(['foo', 'bar'])
})
})

0 comments on commit 958b91b

Please sign in to comment.