Skip to content

Commit

Permalink
fix: record native events against each wrapper they bubble through (#394
Browse files Browse the repository at this point in the history
)

* fix: record native events against each wrapper they bubble through

* fix: bind to events from dom-event-types intsead of iterating on* keys

* fix: add compositionStart emitted test
  • Loading branch information
Lindsay Gaines authored Feb 19, 2021
1 parent 2444d4b commit 908ec71
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 18 deletions.
8 changes: 4 additions & 4 deletions src/emit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,23 +33,23 @@ export const attachEmitListener = () => {
setDevtoolsHook(createDevTools(events))
}

// devtools hook only catches Vue component custom events
function createDevTools(events: Events): any {
return {
emit(eventType, ...payload) {
if (eventType !== DevtoolsHooks.COMPONENT_EMIT) return

const [rootVM, componentVM, event, eventArgs] = payload
recordEvent(events, componentVM, event, eventArgs)
recordEvent(componentVM, event, eventArgs)
}
} as Partial<typeof devtools>
}

function recordEvent(
events: Events,
export const recordEvent = (
vm: ComponentInternalInstance,
event: string,
args: unknown[]
): void {
): void => {
// Functional component wrapper creates a parent component
let wrapperVm = vm
while (typeof wrapperVm?.type === 'function') wrapperVm = wrapperVm.parent!
Expand Down
21 changes: 19 additions & 2 deletions src/vueWrapper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ComponentPublicInstance, nextTick, App } from 'vue'
import { ShapeFlags } from '@vue/shared'
// @ts-ignore todo - No DefinitelyTyped package exists for this
import eventTypes from 'dom-event-types'

import { config } from './config'
import { DOMWrapper } from './domWrapper'
Expand All @@ -11,7 +13,7 @@ import {
import { createWrapperError } from './errorWrapper'
import { find, matches } from './utils/find'
import { mergeDeep } from './utils'
import { emitted } from './emit'
import { emitted, recordEvent } from './emit'
import BaseWrapper from './baseWrapper'
import WrapperLike from './interfaces/wrapperLike'

Expand All @@ -34,6 +36,9 @@ export class VueWrapper<T extends ComponentPublicInstance>
this.rootVM = vm?.$root
this.componentVM = vm as T
this.__setProps = setProps

this.attachNativeEventListener()

config.plugins.VueWrapper.extend(this)
}

Expand All @@ -46,6 +51,18 @@ export class VueWrapper<T extends ComponentPublicInstance>
return this.vm.$el.parentElement
}

private attachNativeEventListener(): void {
const vm = this.vm
if (!vm) return

const element = this.element
for (let eventName of Object.keys(eventTypes)) {
element.addEventListener(eventName, (...args) => {
recordEvent(vm.$, eventName, args)
})
}
}

get element(): Element {
// if the component has multiple root elements, we use the parent's element
return this.hasMultipleRoots ? this.parentElement : this.vm.$el
Expand All @@ -63,7 +80,7 @@ export class VueWrapper<T extends ComponentPublicInstance>
}

emitted<T = unknown>(): Record<string, T[]>
emitted<T = unknown>(eventName?: string): undefined | T[]
emitted<T = unknown>(eventName: string): undefined | T[]
emitted<T = unknown>(
eventName?: string
): undefined | T[] | Record<string, T[]> {
Expand Down
109 changes: 97 additions & 12 deletions tests/emit.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { defineComponent, FunctionalComponent, h, SetupContext } from 'vue'
import {
defineComponent,
FunctionalComponent,
getCurrentInstance,
h,
SetupContext
} from 'vue'
import { Vue } from 'vue-class-component'

import { mount } from '../src'
Expand Down Expand Up @@ -78,23 +84,73 @@ describe('emitted', () => {
expect(wrapper.emitted().hello[1]).toEqual(['foo', 'bar'])
})

it('should not propagate child events', () => {
it('should propagate child native events', async () => {
const Child = defineComponent({
name: 'Child',
setup(props, { emit }) {
name: 'Button',
setup(props) {
return () => h('div', [h('input')])
}
})

const Parent = defineComponent({
name: 'Parent',
setup() {
return () =>
h('div', [
h('button', { onClick: () => emit('hello', 'foo', 'bar') })
h(Child, {
onClick: (event: Event) => event.stopPropagation()
})
])
}
})

const GrandParent: FunctionalComponent<{ level: number }> = (
props,
ctx
) => {
return h(`h${props.level}`, [h(Parent)])
}

const wrapper = mount(GrandParent)
const parentWrapper = wrapper.findComponent(Parent)
const childWrapper = wrapper.findComponent(Child)
const input = wrapper.find('input')

expect(wrapper.emitted()).toEqual({})
expect(parentWrapper.emitted()).toEqual({})
expect(childWrapper.emitted()).toEqual({})

await input.trigger('click')

// Propagation should stop at Parent
expect(childWrapper.emitted().click).toHaveLength(1)
expect(parentWrapper.emitted().click).toEqual(undefined)
expect(wrapper.emitted().click).toEqual(undefined)

input.setValue('hey')

expect(childWrapper.emitted().input).toHaveLength(1)
expect(parentWrapper.emitted().input).toHaveLength(1)
expect(wrapper.emitted().input).toHaveLength(1)
})

it('should not propagate child custom events', () => {
const Child = defineComponent({
name: 'Child',
emits: ['hi'],
setup(props, { emit }) {
return () =>
h('div', [h('button', { onClick: () => emit('hi', 'foo', 'bar') })])
}
})

const Parent = defineComponent({
name: 'Parent',
emits: ['hello'],
setup(props, { emit }) {
return () =>
h(Child, {
onHello: (...events: unknown[]) => emit('parent', ...events)
onHi: (...events: unknown[]) => emit('hello', ...events)
})
}
})
Expand All @@ -105,14 +161,18 @@ describe('emitted', () => {
expect(childWrapper.emitted()).toEqual({})

wrapper.find('button').trigger('click')
expect(wrapper.emitted().parent[0]).toEqual(['foo', 'bar'])
expect(wrapper.emitted().hello).toEqual(undefined)
expect(childWrapper.emitted().hello[0]).toEqual(['foo', 'bar'])

// Parent should emit custom event 'hello' but not 'hi'
expect(wrapper.emitted().hello[0]).toEqual(['foo', 'bar'])
expect(wrapper.emitted().hi).toEqual(undefined)
// Child should emit custom event 'hi'
expect(childWrapper.emitted().hi[0]).toEqual(['foo', 'bar'])

// Additional events should accumulate in the same format
wrapper.find('button').trigger('click')
expect(wrapper.emitted().parent[1]).toEqual(['foo', 'bar'])
expect(wrapper.emitted().hello).toEqual(undefined)
expect(childWrapper.emitted().hello[1]).toEqual(['foo', 'bar'])
expect(wrapper.emitted().hello[1]).toEqual(['foo', 'bar'])
expect(wrapper.emitted().hi).toEqual(undefined)
expect(childWrapper.emitted().hi[1]).toEqual(['foo', 'bar'])
})

it('should allow passing the name of an event', () => {
Expand Down Expand Up @@ -189,4 +249,29 @@ describe('emitted', () => {
const wrapper = mount(Comp)
expect(wrapper.emitted().foo).toBeTruthy()
})

it('captures composition event', async () => {
const useCommonBindings = () => {
const onCompositionStart = (evt: CompositionEvent) => {
const instance = getCurrentInstance()!
instance.emit('compositionStart', evt)
}
return { onCompositionStart }
}
const IxInput = defineComponent({
setup() {
return useCommonBindings()
},
template: `<input @compositionstart="(evt) => $emit('compositionStart', evt)" />`
})

const wrapper = mount({
components: { IxInput },
template: `<ix-input />`
})

await wrapper.find('input').trigger('compositionstart')

expect(wrapper.emitted().compositionstart).not.toBe(undefined)
})
})

0 comments on commit 908ec71

Please sign in to comment.