Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(event): be robust against incomplete event implementations #1009

Merged
merged 1 commit into from Aug 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
276 changes: 206 additions & 70 deletions src/event/createEvent.ts
@@ -1,98 +1,234 @@
import {createEvent as createEventBase} from '@testing-library/dom'
import {eventMap, eventMapKeys, isMouseEvent} from './eventMap'
import {EventType, PointerCoords} from './types'
import {getWindow} from '../utils'
import {eventMap, eventMapKeys} from './eventMap'
import type {EventType, EventTypeInit, FixedDocumentEventMap} from './types'

export type EventTypeInit<K extends EventType> = SpecificEventInit<
FixedDocumentEventMap[K]
>

interface FixedDocumentEventMap extends DocumentEventMap {
input: InputEvent
}

type SpecificEventInit<E extends Event> = E extends InputEvent
? InputEventInit
: E extends ClipboardEvent
? ClipboardEventInit
: E extends KeyboardEvent
? KeyboardEventInit
: E extends PointerEvent
? PointerEventInit
: E extends MouseEvent
? MouseEventInit
: E extends UIEvent
? UIEventInit
: EventInit
const eventInitializer = {
ClipboardEvent: [initClipboardEvent],
InputEvent: [initUIEvent, initInputEvent],
MouseEvent: [initUIEvent, initUIEventModififiers, initMouseEvent],
PointerEvent: [
initUIEvent,
initUIEventModififiers,
initMouseEvent,
initPointerEvent,
],
KeyboardEvent: [initUIEvent, initUIEventModififiers, initKeyboardEvent],
} as Record<EventInterface, undefined | Array<(e: Event, i: EventInit) => void>>

export function createEvent<K extends EventType>(
type: K,
target: Element,
init?: EventTypeInit<K>,
) {
const event = createEventBase(
type,
target,
init,
eventMap[eventMapKeys[type] as keyof typeof eventMap],
) as DocumentEventMap[K]
const window = getWindow(target)
const {EventType, defaultInit} =
eventMap[eventMapKeys[type] as keyof typeof eventMap]
const event = new (getEventConstructors(window)[EventType])(type, defaultInit)
eventInitializer[EventType]?.forEach(f => f(event, init ?? {}))

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

return event
/* istanbul ignore next */
function getEventConstructors(window: Window & typeof globalThis) {
/* eslint-disable @typescript-eslint/no-unnecessary-condition, @typescript-eslint/no-extraneous-class */
const Event = window.Event ?? class Event {}
const AnimationEvent =
window.AnimationEvent ?? class AnimationEvent extends Event {}
const ClipboardEvent =
window.ClipboardEvent ?? class ClipboardEvent extends Event {}
const PopStateEvent =
window.PopStateEvent ?? class PopStateEvent extends Event {}
const ProgressEvent =
window.ProgressEvent ?? class ProgressEvent extends Event {}
const TransitionEvent =
window.TransitionEvent ?? class TransitionEvent extends Event {}
const UIEvent = window.UIEvent ?? class UIEvent extends Event {}
const CompositionEvent =
window.CompositionEvent ?? class CompositionEvent extends UIEvent {}
const FocusEvent = window.FocusEvent ?? class FocusEvent extends UIEvent {}
const InputEvent = window.InputEvent ?? class InputEvent extends UIEvent {}
const KeyboardEvent =
window.KeyboardEvent ?? class KeyboardEvent extends UIEvent {}
const MouseEvent = window.MouseEvent ?? class MouseEvent extends UIEvent {}
const DragEvent = window.DragEvent ?? class DragEvent extends MouseEvent {}
const PointerEvent =
window.PointerEvent ?? class PointerEvent extends MouseEvent {}
const TouchEvent = window.TouchEvent ?? class TouchEvent extends UIEvent {}
/* eslint-enable @typescript-eslint/no-unnecessary-condition, @typescript-eslint/no-extraneous-class */

return {
Event,
AnimationEvent,
ClipboardEvent,
PopStateEvent,
ProgressEvent,
TransitionEvent,
UIEvent,
CompositionEvent,
FocusEvent,
InputEvent,
KeyboardEvent,
MouseEvent,
DragEvent,
PointerEvent,
TouchEvent,
}
}

function assignProps(
obj: MouseEvent | PointerEvent,
props: MouseEventInit & PointerEventInit & PointerCoords,
) {
function assignProps<T extends object>(obj: T, props: {[k in keyof T]?: T[k]}) {
for (const [key, value] of Object.entries(props)) {
Object.defineProperty(obj, key, {get: () => value})
Object.defineProperty(obj, key, {get: () => value ?? null})
}
}

function assignPositionInit(
obj: MouseEvent | PointerEvent,
function sanitizeNumber(n: number | undefined) {
return Number(n ?? 0)
}

function initClipboardEvent(
event: ClipboardEvent,
{clipboardData}: ClipboardEventInit,
) {
assignProps(event, {
clipboardData,
})
}

function initInputEvent(
event: InputEvent,
{data, inputType, isComposing}: InputEventInit,
) {
assignProps(event, {
data,
isComposing: Boolean(isComposing),
inputType: String(inputType),
})
}

function initUIEvent(event: UIEvent, {view, detail}: UIEventInit) {
assignProps(event, {
view,
detail: sanitizeNumber(detail ?? 0),
})
}

function initUIEventModififiers(
event: KeyboardEvent | MouseEvent,
{
altKey,
ctrlKey,
metaKey,
shiftKey,
modifierAltGraph,
modifierCapsLock,
modifierFn,
modifierFnLock,
modifierNumLock,
modifierScrollLock,
modifierSymbol,
modifierSymbolLock,
}: EventModifierInit,
) {
assignProps(event, {
altKey: Boolean(altKey),
ctrlKey: Boolean(ctrlKey),
metaKey: Boolean(metaKey),
shiftKey: Boolean(shiftKey),
getModifierState(k: string) {
return Boolean(
{
Alt: altKey,
AltGraph: modifierAltGraph,
CapsLock: modifierCapsLock,
Control: ctrlKey,
Fn: modifierFn,
FnLock: modifierFnLock,
Meta: metaKey,
NumLock: modifierNumLock,
ScrollLock: modifierScrollLock,
Shift: shiftKey,
Symbol: modifierSymbol,
SymbolLock: modifierSymbolLock,
}[k],
)
},
})
}

function initKeyboardEvent(
event: KeyboardEvent,
{
key,
code,
location,
repeat,
isComposing,
charCode, // `charCode` is necessary for React17 `keypress`
}: KeyboardEventInit,
) {
assignProps(event, {
key: String(key),
code: String(code),
location: sanitizeNumber(location),
repeat: Boolean(repeat),
isComposing: Boolean(isComposing),
charCode,
})
}

function initMouseEvent(
event: MouseEvent,
{
x,
y,
clientX,
clientY,
offsetX,
offsetY,
pageX,
pageY,
screenX,
screenY,
}: PointerCoords & MouseEventInit,
clientX = x,
clientY = y,
button,
buttons,
relatedTarget,
}: MouseEventInit & {x?: number; y?: number},
) {
assignProps(obj, {
/* istanbul ignore start */
x: x ?? clientX ?? 0,
y: y ?? clientY ?? 0,
clientX: x ?? clientX ?? 0,
clientY: y ?? clientY ?? 0,
offsetX: offsetX ?? 0,
offsetY: offsetY ?? 0,
pageX: pageX ?? 0,
pageY: pageY ?? 0,
screenX: screenX ?? 0,
screenY: screenY ?? 0,
/* istanbul ignore end */
assignProps(event, {
screenX: sanitizeNumber(screenX),
screenY: sanitizeNumber(screenY),
clientX: sanitizeNumber(clientX),
x: sanitizeNumber(clientX),
clientY: sanitizeNumber(clientY),
y: sanitizeNumber(clientY),
button: sanitizeNumber(button),
buttons: sanitizeNumber(buttons),
relatedTarget,
})
}

function assignPointerInit(
obj: MouseEvent | PointerEvent,
{isPrimary, pointerId, pointerType}: PointerEventInit,
) {
assignProps(obj, {
isPrimary,
function initPointerEvent(
event: PointerEvent,
{
pointerId,
width,
height,
pressure,
tangentialPressure,
tiltX,
tiltY,
twist,
pointerType,
isPrimary,
}: PointerEventInit,
) {
assignProps(event, {
pointerId: sanitizeNumber(pointerId),
width: sanitizeNumber(width),
height: sanitizeNumber(height),
pressure: sanitizeNumber(pressure),
tangentialPressure: sanitizeNumber(tangentialPressure),
tiltX: sanitizeNumber(tiltX),
tiltY: sanitizeNumber(tiltY),
twist: sanitizeNumber(twist),
pointerType: String(pointerType),
isPrimary: Boolean(isPrimary),
})
}
19 changes: 18 additions & 1 deletion src/event/dom-events.d.ts
Expand Up @@ -2,8 +2,25 @@ declare module '@testing-library/dom/dist/event-map.js' {
import {EventType} from '@testing-library/dom'
export const eventMap: {
[k in EventType]: {
EventType: string
EventType: EventInterface
defaultInit: EventInit
}
}
}

type EventInterface =
| 'AnimationEvent'
| 'ClipboardEvent'
| 'CompositionEvent'
| 'DragEvent'
| 'Event'
| 'FocusEvent'
| 'InputEvent'
| 'KeyboardEvent'
| 'MouseEvent'
| 'PointerEvent'
| 'PopStateEvent'
| 'ProgressEvent'
| 'TouchEvent'
| 'TransitionEvent'
| 'UIEvent'
14 changes: 10 additions & 4 deletions src/event/eventMap.ts
Expand Up @@ -4,17 +4,23 @@ import {EventType} from './types'
export const eventMap = {
...baseEventMap,

click: {
EventType: 'PointerEvent',
defaultInit: {bubbles: true, cancelable: true, composed: true},
},
auxclick: {
// like other events this should be PointerEvent, but this is missing in Jsdom
// see https://github.com/jsdom/jsdom/issues/2527
EventType: 'MouseEvent',
EventType: 'PointerEvent',
defaultInit: {bubbles: true, cancelable: true, composed: true},
},
contextmenu: {
EventType: 'PointerEvent',
defaultInit: {bubbles: true, cancelable: true, composed: true},
},
beforeInput: {
EventType: 'InputEvent',
defaultInit: {bubbles: true, cancelable: true, composed: true},
},
}
} as const

export const eventMapKeys: {
[k in keyof DocumentEventMap]?: keyof typeof eventMap
Expand Down
4 changes: 2 additions & 2 deletions src/event/index.ts
@@ -1,8 +1,8 @@
import {Config} from '../setup'
import {createEvent, EventTypeInit} from './createEvent'
import {createEvent} from './createEvent'
import {dispatchEvent} from './dispatchEvent'
import {isKeyboardEvent, isMouseEvent} from './eventMap'
import {EventType, PointerCoords} from './types'
import {EventType, EventTypeInit, PointerCoords} from './types'

export type {EventType, PointerCoords}

Expand Down
22 changes: 22 additions & 0 deletions src/event/types.ts
@@ -1,5 +1,27 @@
export type EventType = keyof DocumentEventMap

export type EventTypeInit<K extends EventType> = SpecificEventInit<
FixedDocumentEventMap[K]
>

export interface FixedDocumentEventMap extends DocumentEventMap {
input: InputEvent
}

type SpecificEventInit<E extends Event> = E extends InputEvent
? InputEventInit
: E extends ClipboardEvent
? ClipboardEventInit
: E extends KeyboardEvent
? KeyboardEventInit
: E extends PointerEvent
? PointerEventInit
: E extends MouseEvent
? MouseEventInit
: E extends UIEvent
? UIEventInit
: EventInit

export interface PointerCoords {
x?: number
y?: number
Expand Down