Skip to content

Commit 289828b

Browse files
authored
fix(event): be robust against incomplete event implementations (#1009)
1 parent a46b4d7 commit 289828b

File tree

7 files changed

+261
-79
lines changed

7 files changed

+261
-79
lines changed

src/event/createEvent.ts

+206-70
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,234 @@
1-
import {createEvent as createEventBase} from '@testing-library/dom'
2-
import {eventMap, eventMapKeys, isMouseEvent} from './eventMap'
3-
import {EventType, PointerCoords} from './types'
1+
import {getWindow} from '../utils'
2+
import {eventMap, eventMapKeys} from './eventMap'
3+
import type {EventType, EventTypeInit, FixedDocumentEventMap} from './types'
44

5-
export type EventTypeInit<K extends EventType> = SpecificEventInit<
6-
FixedDocumentEventMap[K]
7-
>
8-
9-
interface FixedDocumentEventMap extends DocumentEventMap {
10-
input: InputEvent
11-
}
12-
13-
type SpecificEventInit<E extends Event> = E extends InputEvent
14-
? InputEventInit
15-
: E extends ClipboardEvent
16-
? ClipboardEventInit
17-
: E extends KeyboardEvent
18-
? KeyboardEventInit
19-
: E extends PointerEvent
20-
? PointerEventInit
21-
: E extends MouseEvent
22-
? MouseEventInit
23-
: E extends UIEvent
24-
? UIEventInit
25-
: EventInit
5+
const eventInitializer = {
6+
ClipboardEvent: [initClipboardEvent],
7+
InputEvent: [initUIEvent, initInputEvent],
8+
MouseEvent: [initUIEvent, initUIEventModififiers, initMouseEvent],
9+
PointerEvent: [
10+
initUIEvent,
11+
initUIEventModififiers,
12+
initMouseEvent,
13+
initPointerEvent,
14+
],
15+
KeyboardEvent: [initUIEvent, initUIEventModififiers, initKeyboardEvent],
16+
} as Record<EventInterface, undefined | Array<(e: Event, i: EventInit) => void>>
2617

2718
export function createEvent<K extends EventType>(
2819
type: K,
2920
target: Element,
3021
init?: EventTypeInit<K>,
3122
) {
32-
const event = createEventBase(
33-
type,
34-
target,
35-
init,
36-
eventMap[eventMapKeys[type] as keyof typeof eventMap],
37-
) as DocumentEventMap[K]
23+
const window = getWindow(target)
24+
const {EventType, defaultInit} =
25+
eventMap[eventMapKeys[type] as keyof typeof eventMap]
26+
const event = new (getEventConstructors(window)[EventType])(type, defaultInit)
27+
eventInitializer[EventType]?.forEach(f => f(event, init ?? {}))
3828

39-
// Can not use instanceof, as MouseEvent might be polyfilled.
40-
if (isMouseEvent(type) && init) {
41-
// see https://github.com/testing-library/react-testing-library/issues/268
42-
assignPositionInit(event as MouseEvent, init)
43-
assignPointerInit(event as PointerEvent, init)
44-
}
29+
return event as FixedDocumentEventMap[K]
30+
}
4531

46-
return event
32+
/* istanbul ignore next */
33+
function getEventConstructors(window: Window & typeof globalThis) {
34+
/* eslint-disable @typescript-eslint/no-unnecessary-condition, @typescript-eslint/no-extraneous-class */
35+
const Event = window.Event ?? class Event {}
36+
const AnimationEvent =
37+
window.AnimationEvent ?? class AnimationEvent extends Event {}
38+
const ClipboardEvent =
39+
window.ClipboardEvent ?? class ClipboardEvent extends Event {}
40+
const PopStateEvent =
41+
window.PopStateEvent ?? class PopStateEvent extends Event {}
42+
const ProgressEvent =
43+
window.ProgressEvent ?? class ProgressEvent extends Event {}
44+
const TransitionEvent =
45+
window.TransitionEvent ?? class TransitionEvent extends Event {}
46+
const UIEvent = window.UIEvent ?? class UIEvent extends Event {}
47+
const CompositionEvent =
48+
window.CompositionEvent ?? class CompositionEvent extends UIEvent {}
49+
const FocusEvent = window.FocusEvent ?? class FocusEvent extends UIEvent {}
50+
const InputEvent = window.InputEvent ?? class InputEvent extends UIEvent {}
51+
const KeyboardEvent =
52+
window.KeyboardEvent ?? class KeyboardEvent extends UIEvent {}
53+
const MouseEvent = window.MouseEvent ?? class MouseEvent extends UIEvent {}
54+
const DragEvent = window.DragEvent ?? class DragEvent extends MouseEvent {}
55+
const PointerEvent =
56+
window.PointerEvent ?? class PointerEvent extends MouseEvent {}
57+
const TouchEvent = window.TouchEvent ?? class TouchEvent extends UIEvent {}
58+
/* eslint-enable @typescript-eslint/no-unnecessary-condition, @typescript-eslint/no-extraneous-class */
59+
60+
return {
61+
Event,
62+
AnimationEvent,
63+
ClipboardEvent,
64+
PopStateEvent,
65+
ProgressEvent,
66+
TransitionEvent,
67+
UIEvent,
68+
CompositionEvent,
69+
FocusEvent,
70+
InputEvent,
71+
KeyboardEvent,
72+
MouseEvent,
73+
DragEvent,
74+
PointerEvent,
75+
TouchEvent,
76+
}
4777
}
4878

49-
function assignProps(
50-
obj: MouseEvent | PointerEvent,
51-
props: MouseEventInit & PointerEventInit & PointerCoords,
52-
) {
79+
function assignProps<T extends object>(obj: T, props: {[k in keyof T]?: T[k]}) {
5380
for (const [key, value] of Object.entries(props)) {
54-
Object.defineProperty(obj, key, {get: () => value})
81+
Object.defineProperty(obj, key, {get: () => value ?? null})
5582
}
5683
}
5784

58-
function assignPositionInit(
59-
obj: MouseEvent | PointerEvent,
85+
function sanitizeNumber(n: number | undefined) {
86+
return Number(n ?? 0)
87+
}
88+
89+
function initClipboardEvent(
90+
event: ClipboardEvent,
91+
{clipboardData}: ClipboardEventInit,
92+
) {
93+
assignProps(event, {
94+
clipboardData,
95+
})
96+
}
97+
98+
function initInputEvent(
99+
event: InputEvent,
100+
{data, inputType, isComposing}: InputEventInit,
101+
) {
102+
assignProps(event, {
103+
data,
104+
isComposing: Boolean(isComposing),
105+
inputType: String(inputType),
106+
})
107+
}
108+
109+
function initUIEvent(event: UIEvent, {view, detail}: UIEventInit) {
110+
assignProps(event, {
111+
view,
112+
detail: sanitizeNumber(detail ?? 0),
113+
})
114+
}
115+
116+
function initUIEventModififiers(
117+
event: KeyboardEvent | MouseEvent,
118+
{
119+
altKey,
120+
ctrlKey,
121+
metaKey,
122+
shiftKey,
123+
modifierAltGraph,
124+
modifierCapsLock,
125+
modifierFn,
126+
modifierFnLock,
127+
modifierNumLock,
128+
modifierScrollLock,
129+
modifierSymbol,
130+
modifierSymbolLock,
131+
}: EventModifierInit,
132+
) {
133+
assignProps(event, {
134+
altKey: Boolean(altKey),
135+
ctrlKey: Boolean(ctrlKey),
136+
metaKey: Boolean(metaKey),
137+
shiftKey: Boolean(shiftKey),
138+
getModifierState(k: string) {
139+
return Boolean(
140+
{
141+
Alt: altKey,
142+
AltGraph: modifierAltGraph,
143+
CapsLock: modifierCapsLock,
144+
Control: ctrlKey,
145+
Fn: modifierFn,
146+
FnLock: modifierFnLock,
147+
Meta: metaKey,
148+
NumLock: modifierNumLock,
149+
ScrollLock: modifierScrollLock,
150+
Shift: shiftKey,
151+
Symbol: modifierSymbol,
152+
SymbolLock: modifierSymbolLock,
153+
}[k],
154+
)
155+
},
156+
})
157+
}
158+
159+
function initKeyboardEvent(
160+
event: KeyboardEvent,
161+
{
162+
key,
163+
code,
164+
location,
165+
repeat,
166+
isComposing,
167+
charCode, // `charCode` is necessary for React17 `keypress`
168+
}: KeyboardEventInit,
169+
) {
170+
assignProps(event, {
171+
key: String(key),
172+
code: String(code),
173+
location: sanitizeNumber(location),
174+
repeat: Boolean(repeat),
175+
isComposing: Boolean(isComposing),
176+
charCode,
177+
})
178+
}
179+
180+
function initMouseEvent(
181+
event: MouseEvent,
60182
{
61183
x,
62184
y,
63-
clientX,
64-
clientY,
65-
offsetX,
66-
offsetY,
67-
pageX,
68-
pageY,
69185
screenX,
70186
screenY,
71-
}: PointerCoords & MouseEventInit,
187+
clientX = x,
188+
clientY = y,
189+
button,
190+
buttons,
191+
relatedTarget,
192+
}: MouseEventInit & {x?: number; y?: number},
72193
) {
73-
assignProps(obj, {
74-
/* istanbul ignore start */
75-
x: x ?? clientX ?? 0,
76-
y: y ?? clientY ?? 0,
77-
clientX: x ?? clientX ?? 0,
78-
clientY: y ?? clientY ?? 0,
79-
offsetX: offsetX ?? 0,
80-
offsetY: offsetY ?? 0,
81-
pageX: pageX ?? 0,
82-
pageY: pageY ?? 0,
83-
screenX: screenX ?? 0,
84-
screenY: screenY ?? 0,
85-
/* istanbul ignore end */
194+
assignProps(event, {
195+
screenX: sanitizeNumber(screenX),
196+
screenY: sanitizeNumber(screenY),
197+
clientX: sanitizeNumber(clientX),
198+
x: sanitizeNumber(clientX),
199+
clientY: sanitizeNumber(clientY),
200+
y: sanitizeNumber(clientY),
201+
button: sanitizeNumber(button),
202+
buttons: sanitizeNumber(buttons),
203+
relatedTarget,
86204
})
87205
}
88206

89-
function assignPointerInit(
90-
obj: MouseEvent | PointerEvent,
91-
{isPrimary, pointerId, pointerType}: PointerEventInit,
92-
) {
93-
assignProps(obj, {
94-
isPrimary,
207+
function initPointerEvent(
208+
event: PointerEvent,
209+
{
95210
pointerId,
211+
width,
212+
height,
213+
pressure,
214+
tangentialPressure,
215+
tiltX,
216+
tiltY,
217+
twist,
96218
pointerType,
219+
isPrimary,
220+
}: PointerEventInit,
221+
) {
222+
assignProps(event, {
223+
pointerId: sanitizeNumber(pointerId),
224+
width: sanitizeNumber(width),
225+
height: sanitizeNumber(height),
226+
pressure: sanitizeNumber(pressure),
227+
tangentialPressure: sanitizeNumber(tangentialPressure),
228+
tiltX: sanitizeNumber(tiltX),
229+
tiltY: sanitizeNumber(tiltY),
230+
twist: sanitizeNumber(twist),
231+
pointerType: String(pointerType),
232+
isPrimary: Boolean(isPrimary),
97233
})
98234
}

src/event/dom-events.d.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,25 @@ declare module '@testing-library/dom/dist/event-map.js' {
22
import {EventType} from '@testing-library/dom'
33
export const eventMap: {
44
[k in EventType]: {
5-
EventType: string
5+
EventType: EventInterface
66
defaultInit: EventInit
77
}
88
}
99
}
10+
11+
type EventInterface =
12+
| 'AnimationEvent'
13+
| 'ClipboardEvent'
14+
| 'CompositionEvent'
15+
| 'DragEvent'
16+
| 'Event'
17+
| 'FocusEvent'
18+
| 'InputEvent'
19+
| 'KeyboardEvent'
20+
| 'MouseEvent'
21+
| 'PointerEvent'
22+
| 'PopStateEvent'
23+
| 'ProgressEvent'
24+
| 'TouchEvent'
25+
| 'TransitionEvent'
26+
| 'UIEvent'

src/event/eventMap.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,23 @@ import {EventType} from './types'
44
export const eventMap = {
55
...baseEventMap,
66

7+
click: {
8+
EventType: 'PointerEvent',
9+
defaultInit: {bubbles: true, cancelable: true, composed: true},
10+
},
711
auxclick: {
8-
// like other events this should be PointerEvent, but this is missing in Jsdom
9-
// see https://github.com/jsdom/jsdom/issues/2527
10-
EventType: 'MouseEvent',
12+
EventType: 'PointerEvent',
13+
defaultInit: {bubbles: true, cancelable: true, composed: true},
14+
},
15+
contextmenu: {
16+
EventType: 'PointerEvent',
1117
defaultInit: {bubbles: true, cancelable: true, composed: true},
1218
},
1319
beforeInput: {
1420
EventType: 'InputEvent',
1521
defaultInit: {bubbles: true, cancelable: true, composed: true},
1622
},
17-
}
23+
} as const
1824

1925
export const eventMapKeys: {
2026
[k in keyof DocumentEventMap]?: keyof typeof eventMap

src/event/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import {Config} from '../setup'
2-
import {createEvent, EventTypeInit} from './createEvent'
2+
import {createEvent} from './createEvent'
33
import {dispatchEvent} from './dispatchEvent'
44
import {isKeyboardEvent, isMouseEvent} from './eventMap'
5-
import {EventType, PointerCoords} from './types'
5+
import {EventType, EventTypeInit, PointerCoords} from './types'
66

77
export type {EventType, PointerCoords}
88

src/event/types.ts

+22
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
export type EventType = keyof DocumentEventMap
22

3+
export type EventTypeInit<K extends EventType> = SpecificEventInit<
4+
FixedDocumentEventMap[K]
5+
>
6+
7+
export interface FixedDocumentEventMap extends DocumentEventMap {
8+
input: InputEvent
9+
}
10+
11+
type SpecificEventInit<E extends Event> = E extends InputEvent
12+
? InputEventInit
13+
: E extends ClipboardEvent
14+
? ClipboardEventInit
15+
: E extends KeyboardEvent
16+
? KeyboardEventInit
17+
: E extends PointerEvent
18+
? PointerEventInit
19+
: E extends MouseEvent
20+
? MouseEventInit
21+
: E extends UIEvent
22+
? UIEventInit
23+
: EventInit
24+
325
export interface PointerCoords {
426
x?: number
527
y?: number

0 commit comments

Comments
 (0)