Skip to content

Commit 908ec71

Browse files
author
Lindsay Gaines
authored
fix: record native events against each wrapper they bubble through (vuejs#394)
* 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
1 parent 2444d4b commit 908ec71

File tree

3 files changed

+120
-18
lines changed

3 files changed

+120
-18
lines changed

src/emit.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,23 +33,23 @@ export const attachEmitListener = () => {
3333
setDevtoolsHook(createDevTools(events))
3434
}
3535

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

4142
const [rootVM, componentVM, event, eventArgs] = payload
42-
recordEvent(events, componentVM, event, eventArgs)
43+
recordEvent(componentVM, event, eventArgs)
4344
}
4445
} as Partial<typeof devtools>
4546
}
4647

47-
function recordEvent(
48-
events: Events,
48+
export const recordEvent = (
4949
vm: ComponentInternalInstance,
5050
event: string,
5151
args: unknown[]
52-
): void {
52+
): void => {
5353
// Functional component wrapper creates a parent component
5454
let wrapperVm = vm
5555
while (typeof wrapperVm?.type === 'function') wrapperVm = wrapperVm.parent!

src/vueWrapper.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { ComponentPublicInstance, nextTick, App } from 'vue'
22
import { ShapeFlags } from '@vue/shared'
3+
// @ts-ignore todo - No DefinitelyTyped package exists for this
4+
import eventTypes from 'dom-event-types'
35

46
import { config } from './config'
57
import { DOMWrapper } from './domWrapper'
@@ -11,7 +13,7 @@ import {
1113
import { createWrapperError } from './errorWrapper'
1214
import { find, matches } from './utils/find'
1315
import { mergeDeep } from './utils'
14-
import { emitted } from './emit'
16+
import { emitted, recordEvent } from './emit'
1517
import BaseWrapper from './baseWrapper'
1618
import WrapperLike from './interfaces/wrapperLike'
1719

@@ -34,6 +36,9 @@ export class VueWrapper<T extends ComponentPublicInstance>
3436
this.rootVM = vm?.$root
3537
this.componentVM = vm as T
3638
this.__setProps = setProps
39+
40+
this.attachNativeEventListener()
41+
3742
config.plugins.VueWrapper.extend(this)
3843
}
3944

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

54+
private attachNativeEventListener(): void {
55+
const vm = this.vm
56+
if (!vm) return
57+
58+
const element = this.element
59+
for (let eventName of Object.keys(eventTypes)) {
60+
element.addEventListener(eventName, (...args) => {
61+
recordEvent(vm.$, eventName, args)
62+
})
63+
}
64+
}
65+
4966
get element(): Element {
5067
// if the component has multiple root elements, we use the parent's element
5168
return this.hasMultipleRoots ? this.parentElement : this.vm.$el
@@ -63,7 +80,7 @@ export class VueWrapper<T extends ComponentPublicInstance>
6380
}
6481

6582
emitted<T = unknown>(): Record<string, T[]>
66-
emitted<T = unknown>(eventName?: string): undefined | T[]
83+
emitted<T = unknown>(eventName: string): undefined | T[]
6784
emitted<T = unknown>(
6885
eventName?: string
6986
): undefined | T[] | Record<string, T[]> {

tests/emit.spec.ts

Lines changed: 97 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { defineComponent, FunctionalComponent, h, SetupContext } from 'vue'
1+
import {
2+
defineComponent,
3+
FunctionalComponent,
4+
getCurrentInstance,
5+
h,
6+
SetupContext
7+
} from 'vue'
28
import { Vue } from 'vue-class-component'
39

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

81-
it('should not propagate child events', () => {
87+
it('should propagate child native events', async () => {
8288
const Child = defineComponent({
83-
name: 'Child',
84-
setup(props, { emit }) {
89+
name: 'Button',
90+
setup(props) {
91+
return () => h('div', [h('input')])
92+
}
93+
})
94+
95+
const Parent = defineComponent({
96+
name: 'Parent',
97+
setup() {
8598
return () =>
8699
h('div', [
87-
h('button', { onClick: () => emit('hello', 'foo', 'bar') })
100+
h(Child, {
101+
onClick: (event: Event) => event.stopPropagation()
102+
})
88103
])
89104
}
90105
})
91106

107+
const GrandParent: FunctionalComponent<{ level: number }> = (
108+
props,
109+
ctx
110+
) => {
111+
return h(`h${props.level}`, [h(Parent)])
112+
}
113+
114+
const wrapper = mount(GrandParent)
115+
const parentWrapper = wrapper.findComponent(Parent)
116+
const childWrapper = wrapper.findComponent(Child)
117+
const input = wrapper.find('input')
118+
119+
expect(wrapper.emitted()).toEqual({})
120+
expect(parentWrapper.emitted()).toEqual({})
121+
expect(childWrapper.emitted()).toEqual({})
122+
123+
await input.trigger('click')
124+
125+
// Propagation should stop at Parent
126+
expect(childWrapper.emitted().click).toHaveLength(1)
127+
expect(parentWrapper.emitted().click).toEqual(undefined)
128+
expect(wrapper.emitted().click).toEqual(undefined)
129+
130+
input.setValue('hey')
131+
132+
expect(childWrapper.emitted().input).toHaveLength(1)
133+
expect(parentWrapper.emitted().input).toHaveLength(1)
134+
expect(wrapper.emitted().input).toHaveLength(1)
135+
})
136+
137+
it('should not propagate child custom events', () => {
138+
const Child = defineComponent({
139+
name: 'Child',
140+
emits: ['hi'],
141+
setup(props, { emit }) {
142+
return () =>
143+
h('div', [h('button', { onClick: () => emit('hi', 'foo', 'bar') })])
144+
}
145+
})
146+
92147
const Parent = defineComponent({
93148
name: 'Parent',
149+
emits: ['hello'],
94150
setup(props, { emit }) {
95151
return () =>
96152
h(Child, {
97-
onHello: (...events: unknown[]) => emit('parent', ...events)
153+
onHi: (...events: unknown[]) => emit('hello', ...events)
98154
})
99155
}
100156
})
@@ -105,14 +161,18 @@ describe('emitted', () => {
105161
expect(childWrapper.emitted()).toEqual({})
106162

107163
wrapper.find('button').trigger('click')
108-
expect(wrapper.emitted().parent[0]).toEqual(['foo', 'bar'])
109-
expect(wrapper.emitted().hello).toEqual(undefined)
110-
expect(childWrapper.emitted().hello[0]).toEqual(['foo', 'bar'])
111164

165+
// Parent should emit custom event 'hello' but not 'hi'
166+
expect(wrapper.emitted().hello[0]).toEqual(['foo', 'bar'])
167+
expect(wrapper.emitted().hi).toEqual(undefined)
168+
// Child should emit custom event 'hi'
169+
expect(childWrapper.emitted().hi[0]).toEqual(['foo', 'bar'])
170+
171+
// Additional events should accumulate in the same format
112172
wrapper.find('button').trigger('click')
113-
expect(wrapper.emitted().parent[1]).toEqual(['foo', 'bar'])
114-
expect(wrapper.emitted().hello).toEqual(undefined)
115-
expect(childWrapper.emitted().hello[1]).toEqual(['foo', 'bar'])
173+
expect(wrapper.emitted().hello[1]).toEqual(['foo', 'bar'])
174+
expect(wrapper.emitted().hi).toEqual(undefined)
175+
expect(childWrapper.emitted().hi[1]).toEqual(['foo', 'bar'])
116176
})
117177

118178
it('should allow passing the name of an event', () => {
@@ -189,4 +249,29 @@ describe('emitted', () => {
189249
const wrapper = mount(Comp)
190250
expect(wrapper.emitted().foo).toBeTruthy()
191251
})
252+
253+
it('captures composition event', async () => {
254+
const useCommonBindings = () => {
255+
const onCompositionStart = (evt: CompositionEvent) => {
256+
const instance = getCurrentInstance()!
257+
instance.emit('compositionStart', evt)
258+
}
259+
return { onCompositionStart }
260+
}
261+
const IxInput = defineComponent({
262+
setup() {
263+
return useCommonBindings()
264+
},
265+
template: `<input @compositionstart="(evt) => $emit('compositionStart', evt)" />`
266+
})
267+
268+
const wrapper = mount({
269+
components: { IxInput },
270+
template: `<ix-input />`
271+
})
272+
273+
await wrapper.find('input').trigger('compositionstart')
274+
275+
expect(wrapper.emitted().compositionstart).not.toBe(undefined)
276+
})
192277
})

0 commit comments

Comments
 (0)