From b69732f01bd4d04f376814a0f24f9742d98ac7bf Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Mon, 15 Jun 2020 14:11:00 -0600 Subject: [PATCH] feat(createEvent): make a function that allows creating generic events --- src/__tests__/events.js | 13 ++- src/events.js | 128 +++++++++++++------------- types/events.d.ts | 195 +++++++++++++++++++++------------------- 3 files changed, 184 insertions(+), 152 deletions(-) diff --git a/src/__tests__/events.js b/src/__tests__/events.js index 09eb2e80..6813b0b3 100644 --- a/src/__tests__/events.js +++ b/src/__tests__/events.js @@ -1,5 +1,5 @@ import {eventMap, eventAliasMap} from '../event-map' -import {fireEvent} from '..' +import {fireEvent, createEvent} from '..' const eventTypes = [ { @@ -390,3 +390,14 @@ test('fires events on Document', () => { expect(keyDownSpy).toHaveBeenCalledTimes(1) document.removeEventListener('keydown', keyDownSpy) }) + +test('can create generic events', () => { + const el = document.createElement('div') + const eventName = 'my-custom-event' + const handler = jest.fn() + el.addEventListener(eventName, handler) + const event = createEvent(eventName, el) + fireEvent(el, event) + expect(handler).toHaveBeenCalledTimes(1) + expect(handler).toHaveBeenCalledWith(event) +}) diff --git a/src/events.js b/src/events.js index 58da1f7c..08dde7c9 100644 --- a/src/events.js +++ b/src/events.js @@ -18,72 +18,78 @@ function fireEvent(element, event) { }) } -const createEvent = {} +function createEvent( + eventName, + node, + init, + {EventType = 'Event', defaultInit = {}} = {}, +) { + if (!node) { + throw new Error( + `Unable to fire a "${eventName}" event - please provide a DOM element.`, + ) + } + const eventInit = {...defaultInit, ...init} + const {target: {value, files, ...targetProperties} = {}} = eventInit + if (value !== undefined) { + setNativeValue(node, value) + } + if (files !== undefined) { + // input.files is a read-only property so this is not allowed: + // input.files = [file] + // so we have to use this workaround to set the property + Object.defineProperty(node, 'files', { + configurable: true, + enumerable: true, + writable: true, + value: files, + }) + } + Object.assign(node, targetProperties) + const window = getWindowFromNode(node) + const EventConstructor = window[EventType] || window.Event + let event + /* istanbul ignore else */ + if (typeof EventConstructor === 'function') { + event = new EventConstructor(eventName, eventInit) + } else { + // IE11 polyfill from https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill + event = window.document.createEvent(EventType) + const {bubbles, cancelable, detail, ...otherInit} = eventInit + event.initEvent(eventName, bubbles, cancelable, detail) + Object.keys(otherInit).forEach(eventKey => { + event[eventKey] = otherInit[eventKey] + }) + } + + // DataTransfer is not supported in jsdom: https://github.com/jsdom/jsdom/issues/1568 + const dataTransferProperties = ['dataTransfer', 'clipboardData'] + dataTransferProperties.forEach(dataTransferKey => { + const dataTransferValue = eventInit[dataTransferKey] + + if (typeof dataTransferValue === 'object') { + /* istanbul ignore if */ + if (typeof window.DataTransfer === 'function') { + Object.defineProperty(event, dataTransferKey, { + value: Object.assign(new window.DataTransfer(), dataTransferValue), + }) + } else { + Object.defineProperty(event, dataTransferKey, { + value: dataTransferValue, + }) + } + } + }) + + return event +} Object.keys(eventMap).forEach(key => { const {EventType, defaultInit} = eventMap[key] const eventName = key.toLowerCase() - createEvent[key] = (node, init) => { - if (!node) { - throw new Error( - `Unable to fire a "${key}" event - please provide a DOM element.`, - ) - } - const eventInit = {...defaultInit, ...init} - const {target: {value, files, ...targetProperties} = {}} = eventInit - if (value !== undefined) { - setNativeValue(node, value) - } - if (files !== undefined) { - // input.files is a read-only property so this is not allowed: - // input.files = [file] - // so we have to use this workaround to set the property - Object.defineProperty(node, 'files', { - configurable: true, - enumerable: true, - writable: true, - value: files, - }) - } - Object.assign(node, targetProperties) - const window = getWindowFromNode(node) - const EventConstructor = window[EventType] || window.Event - let event - /* istanbul ignore else */ - if (typeof EventConstructor === 'function') { - event = new EventConstructor(eventName, eventInit) - } else { - // IE11 polyfill from https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill - event = window.document.createEvent(EventType) - const {bubbles, cancelable, detail, ...otherInit} = eventInit - event.initEvent(eventName, bubbles, cancelable, detail) - Object.keys(otherInit).forEach(eventKey => { - event[eventKey] = otherInit[eventKey] - }) - } - - // DataTransfer is not supported in jsdom: https://github.com/jsdom/jsdom/issues/1568 - ['dataTransfer', 'clipboardData'].forEach(dataTransferKey => { - const dataTransferValue = eventInit[dataTransferKey]; - - if (typeof dataTransferValue === 'object') { - /* istanbul ignore if */ - if (typeof window.DataTransfer === 'function') { - Object.defineProperty(event, dataTransferKey, { - value: Object.assign(new window.DataTransfer(), dataTransferValue) - }) - } else { - Object.defineProperty(event, dataTransferKey, { - value: dataTransferValue - }) - } - } - }) - - return event - } - + createEvent[key] = (node, init) => + createEvent(eventName, node, init, {EventType, defaultInit}) fireEvent[key] = (node, init) => fireEvent(node, createEvent[key](node, init)) }) diff --git a/types/events.d.ts b/types/events.d.ts index d9c50cb7..a4123d19 100644 --- a/types/events.d.ts +++ b/types/events.d.ts @@ -1,95 +1,110 @@ export type EventType = - | 'copy' - | 'cut' - | 'paste' - | 'compositionEnd' - | 'compositionStart' - | 'compositionUpdate' - | 'keyDown' - | 'keyPress' - | 'keyUp' - | 'focus' - | 'blur' - | 'focusIn' - | 'focusOut' - | 'change' - | 'input' - | 'invalid' - | 'submit' - | 'reset' - | 'click' - | 'contextMenu' - | 'dblClick' - | 'drag' - | 'dragEnd' - | 'dragEnter' - | 'dragExit' - | 'dragLeave' - | 'dragOver' - | 'dragStart' - | 'drop' - | 'mouseDown' - | 'mouseEnter' - | 'mouseLeave' - | 'mouseMove' - | 'mouseOut' - | 'mouseOver' - | 'mouseUp' - | 'popState' - | 'select' - | 'touchCancel' - | 'touchEnd' - | 'touchMove' - | 'touchStart' - | 'scroll' - | 'wheel' - | 'abort' - | 'canPlay' - | 'canPlayThrough' - | 'durationChange' - | 'emptied' - | 'encrypted' - | 'ended' - | 'loadedData' - | 'loadedMetadata' - | 'loadStart' - | 'pause' - | 'play' - | 'playing' - | 'progress' - | 'rateChange' - | 'seeked' - | 'seeking' - | 'stalled' - | 'suspend' - | 'timeUpdate' - | 'volumeChange' - | 'waiting' - | 'load' - | 'error' - | 'animationStart' - | 'animationEnd' - | 'animationIteration' - | 'transitionEnd' - | 'doubleClick' - | 'pointerOver' - | 'pointerEnter' - | 'pointerDown' - | 'pointerMove' - | 'pointerUp' - | 'pointerCancel' - | 'pointerOut' - | 'pointerLeave' - | 'gotPointerCapture' - | 'lostPointerCapture'; + | 'copy' + | 'cut' + | 'paste' + | 'compositionEnd' + | 'compositionStart' + | 'compositionUpdate' + | 'keyDown' + | 'keyPress' + | 'keyUp' + | 'focus' + | 'blur' + | 'focusIn' + | 'focusOut' + | 'change' + | 'input' + | 'invalid' + | 'submit' + | 'reset' + | 'click' + | 'contextMenu' + | 'dblClick' + | 'drag' + | 'dragEnd' + | 'dragEnter' + | 'dragExit' + | 'dragLeave' + | 'dragOver' + | 'dragStart' + | 'drop' + | 'mouseDown' + | 'mouseEnter' + | 'mouseLeave' + | 'mouseMove' + | 'mouseOut' + | 'mouseOver' + | 'mouseUp' + | 'popState' + | 'select' + | 'touchCancel' + | 'touchEnd' + | 'touchMove' + | 'touchStart' + | 'scroll' + | 'wheel' + | 'abort' + | 'canPlay' + | 'canPlayThrough' + | 'durationChange' + | 'emptied' + | 'encrypted' + | 'ended' + | 'loadedData' + | 'loadedMetadata' + | 'loadStart' + | 'pause' + | 'play' + | 'playing' + | 'progress' + | 'rateChange' + | 'seeked' + | 'seeking' + | 'stalled' + | 'suspend' + | 'timeUpdate' + | 'volumeChange' + | 'waiting' + | 'load' + | 'error' + | 'animationStart' + | 'animationEnd' + | 'animationIteration' + | 'transitionEnd' + | 'doubleClick' + | 'pointerOver' + | 'pointerEnter' + | 'pointerDown' + | 'pointerMove' + | 'pointerUp' + | 'pointerCancel' + | 'pointerOut' + | 'pointerLeave' + | 'gotPointerCapture' + | 'lostPointerCapture' -export type FireFunction = (element: Document | Element | Window | Node, event: Event) => boolean; +export type FireFunction = ( + element: Document | Element | Window | Node, + event: Event, +) => boolean export type FireObject = { - [K in EventType]: (element: Document | Element | Window | Node, options?: {}) => boolean; -}; + [K in EventType]: ( + element: Document | Element | Window | Node, + options?: {}, + ) => boolean +} +export type CreateFunction = ( + eventName: EventType, + node: Document | Element | Window | Node, + init?: {}, + options?: {EventType?: string; defaultInit?: {}}, +) => Event export type CreateObject = { - [K in EventType]: (element: Document | Element | Window | Node, options?: {}) => Event; -}; + [K in EventType]: ( + element: Document | Element | Window | Node, + options?: {}, + ) => Event +} -export const createEvent: CreateObject; -export const fireEvent: FireFunction & FireObject; +export const createEvent: CreateObject +export const fireEvent: FireFunction & FireObject