diff --git a/jest.config.js b/jest.config.js
index 7a7a2b50..8d869c49 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -6,6 +6,7 @@ const {
} = require('kcd-scripts/jest')
module.exports = {
+ resetMocks: true,
collectCoverageFrom,
coveragePathIgnorePatterns: [
...coveragePathIgnorePatterns,
diff --git a/package.json b/package.json
index 18cc9185..b4f93502 100644
--- a/package.json
+++ b/package.json
@@ -51,6 +51,7 @@
"jest-watch-select-projects": "^2.0.0",
"jsdom": "^16.2.2",
"kcd-scripts": "^6.2.0",
+ "redent": "^3.0.0",
"typescript": "^3.9.5"
},
"eslintConfig": {
@@ -59,7 +60,8 @@
"import/prefer-default-export": "off",
"import/no-unassigned-import": "off",
"import/no-useless-path-segments": "off",
- "no-console": "off"
+ "no-console": "off",
+ "no-func-assign": "off"
}
},
"eslintIgnore": [
diff --git a/src/event-map.js b/src/event-map.js
index 98d78e66..0eba4a28 100644
--- a/src/event-map.js
+++ b/src/event-map.js
@@ -1,350 +1,360 @@
export const eventMap = {
- // Clipboard Events
- copy: {
- EventType: 'ClipboardEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- cut: {
- EventType: 'ClipboardEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- paste: {
- EventType: 'ClipboardEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- // Composition Events
- compositionEnd: {
- EventType: 'CompositionEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- compositionStart: {
- EventType: 'CompositionEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- compositionUpdate: {
- EventType: 'CompositionEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- // Keyboard Events
- keyDown: {
- EventType: 'KeyboardEvent',
- defaultInit: {bubbles: true, cancelable: true, charCode: 0, composed: true},
- },
- keyPress: {
- EventType: 'KeyboardEvent',
- defaultInit: {bubbles: true, cancelable: true, charCode: 0, composed: true},
- },
- keyUp: {
- EventType: 'KeyboardEvent',
- defaultInit: {bubbles: true, cancelable: true, charCode: 0, composed: true},
- },
- // Focus Events
- focus: {
- EventType: 'FocusEvent',
- defaultInit: {bubbles: false, cancelable: false, composed: true},
- },
- blur: {
- EventType: 'FocusEvent',
- defaultInit: {bubbles: false, cancelable: false, composed: true},
- },
- focusIn: {
- EventType: 'FocusEvent',
- defaultInit: {bubbles: true, cancelable: false, composed: true},
- },
- focusOut: {
- EventType: 'FocusEvent',
- defaultInit: {bubbles: true, cancelable: false, composed: true},
- },
- // Form Events
- change: {
- EventType: 'Event',
- defaultInit: {bubbles: true, cancelable: false},
- },
- input: {
- EventType: 'InputEvent',
- defaultInit: {bubbles: true, cancelable: false, composed: true},
- },
- invalid: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: true},
- },
- submit: {
- EventType: 'Event',
- defaultInit: {bubbles: true, cancelable: true},
- },
- reset: {
- EventType: 'Event',
- defaultInit: {bubbles: true, cancelable: true},
- },
- // Mouse Events
- click: {
- EventType: 'MouseEvent',
- defaultInit: {bubbles: true, cancelable: true, button: 0, composed: true},
- },
- contextMenu: {
- EventType: 'MouseEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- dblClick: {
- EventType: 'MouseEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- drag: {
- EventType: 'DragEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- dragEnd: {
- EventType: 'DragEvent',
- defaultInit: {bubbles: true, cancelable: false, composed: true},
- },
- dragEnter: {
- EventType: 'DragEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- dragExit: {
- EventType: 'DragEvent',
- defaultInit: {bubbles: true, cancelable: false, composed: true},
- },
- dragLeave: {
- EventType: 'DragEvent',
- defaultInit: {bubbles: true, cancelable: false, composed: true},
- },
- dragOver: {
- EventType: 'DragEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- dragStart: {
- EventType: 'DragEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- drop: {
- EventType: 'DragEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- mouseDown: {
- EventType: 'MouseEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- mouseEnter: {
- EventType: 'MouseEvent',
- defaultInit: {bubbles: false, cancelable: false, composed: true},
- },
- mouseLeave: {
- EventType: 'MouseEvent',
- defaultInit: {bubbles: false, cancelable: false, composed: true},
- },
- mouseMove: {
- EventType: 'MouseEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- mouseOut: {
- EventType: 'MouseEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- mouseOver: {
- EventType: 'MouseEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- mouseUp: {
- EventType: 'MouseEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- // Selection Events
- select: {
- EventType: 'Event',
- defaultInit: {bubbles: true, cancelable: false},
- },
- // Touch Events
- touchCancel: {
- EventType: 'TouchEvent',
- defaultInit: {bubbles: true, cancelable: false, composed: true},
- },
- touchEnd: {
- EventType: 'TouchEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- touchMove: {
- EventType: 'TouchEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- touchStart: {
- EventType: 'TouchEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- // UI Events
- scroll: {
- EventType: 'UIEvent',
- defaultInit: {bubbles: false, cancelable: false},
- },
- // Wheel Events
- wheel: {
- EventType: 'WheelEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- // Media Events
- abort: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- canPlay: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- canPlayThrough: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- durationChange: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- emptied: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- encrypted: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- ended: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- loadedData: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- loadedMetadata: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- loadStart: {
- EventType: 'ProgressEvent',
- defaultInit: {bubbles: false, cancelable: false},
- },
- pause: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- play: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- playing: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- progress: {
- EventType: 'ProgressEvent',
- defaultInit: {bubbles: false, cancelable: false},
- },
- rateChange: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- seeked: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- seeking: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- stalled: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- suspend: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- timeUpdate: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- volumeChange: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- waiting: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- // Image Events
- load: {
- EventType: 'UIEvent',
- defaultInit: {bubbles: false, cancelable: false},
- },
- error: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- // Animation Events
- animationStart: {
- EventType: 'AnimationEvent',
- defaultInit: {bubbles: true, cancelable: false},
- },
- animationEnd: {
- EventType: 'AnimationEvent',
- defaultInit: {bubbles: true, cancelable: false},
- },
- animationIteration: {
- EventType: 'AnimationEvent',
- defaultInit: {bubbles: true, cancelable: false},
- },
- // Transition Events
- transitionEnd: {
- EventType: 'TransitionEvent',
- defaultInit: {bubbles: true, cancelable: true},
- },
- // pointer events
- pointerOver: {
- EventType: 'PointerEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- pointerEnter: {
- EventType: 'PointerEvent',
- defaultInit: {bubbles: false, cancelable: false},
- },
- pointerDown: {
- EventType: 'PointerEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- pointerMove: {
- EventType: 'PointerEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- pointerUp: {
- EventType: 'PointerEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- pointerCancel: {
- EventType: 'PointerEvent',
- defaultInit: {bubbles: true, cancelable: false, composed: true},
- },
- pointerOut: {
- EventType: 'PointerEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- pointerLeave: {
- EventType: 'PointerEvent',
- defaultInit: {bubbles: false, cancelable: false},
- },
- gotPointerCapture: {
- EventType: 'PointerEvent',
- defaultInit: {bubbles: false, cancelable: false, composed: true},
- },
- lostPointerCapture: {
- EventType: 'PointerEvent',
- defaultInit: {bubbles: false, cancelable: false, composed: true},
- },
- // history events
- popState: {
- EventType: 'PopStateEvent',
- defaultInit: {bubbles: true, cancelable: false},
- },
- }
-
- export const eventAliasMap = {
- doubleClick: 'dblClick',
- }
+ // Clipboard Events
+ copy: {
+ EventType: 'ClipboardEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ cut: {
+ EventType: 'ClipboardEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ paste: {
+ EventType: 'ClipboardEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ // Composition Events
+ compositionEnd: {
+ EventType: 'CompositionEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ compositionStart: {
+ EventType: 'CompositionEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ compositionUpdate: {
+ EventType: 'CompositionEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ // Keyboard Events
+ keyDown: {
+ EventType: 'KeyboardEvent',
+ defaultInit: {bubbles: true, cancelable: true, charCode: 0, composed: true},
+ },
+ keyPress: {
+ EventType: 'KeyboardEvent',
+ defaultInit: {bubbles: true, cancelable: true, charCode: 0, composed: true},
+ },
+ keyUp: {
+ EventType: 'KeyboardEvent',
+ defaultInit: {bubbles: true, cancelable: true, charCode: 0, composed: true},
+ },
+ // Focus Events
+ focus: {
+ EventType: 'FocusEvent',
+ defaultInit: {bubbles: false, cancelable: false, composed: true},
+ },
+ blur: {
+ EventType: 'FocusEvent',
+ defaultInit: {bubbles: false, cancelable: false, composed: true},
+ },
+ focusIn: {
+ EventType: 'FocusEvent',
+ defaultInit: {bubbles: true, cancelable: false, composed: true},
+ },
+ focusOut: {
+ EventType: 'FocusEvent',
+ defaultInit: {bubbles: true, cancelable: false, composed: true},
+ },
+ // Form Events
+ change: {
+ EventType: 'Event',
+ defaultInit: {bubbles: true, cancelable: false},
+ },
+ input: {
+ EventType: 'InputEvent',
+ defaultInit: {bubbles: true, cancelable: false, composed: true},
+ },
+ invalid: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: true},
+ },
+ submit: {
+ EventType: 'Event',
+ defaultInit: {bubbles: true, cancelable: true},
+ },
+ reset: {
+ EventType: 'Event',
+ defaultInit: {bubbles: true, cancelable: true},
+ },
+ // Mouse Events
+ click: {
+ EventType: 'MouseEvent',
+ defaultInit: {bubbles: true, cancelable: true, button: 0, composed: true},
+ },
+ contextMenu: {
+ EventType: 'MouseEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ dblClick: {
+ EventType: 'MouseEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ drag: {
+ EventType: 'DragEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ dragEnd: {
+ EventType: 'DragEvent',
+ defaultInit: {bubbles: true, cancelable: false, composed: true},
+ },
+ dragEnter: {
+ EventType: 'DragEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ dragExit: {
+ EventType: 'DragEvent',
+ defaultInit: {bubbles: true, cancelable: false, composed: true},
+ },
+ dragLeave: {
+ EventType: 'DragEvent',
+ defaultInit: {bubbles: true, cancelable: false, composed: true},
+ },
+ dragOver: {
+ EventType: 'DragEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ dragStart: {
+ EventType: 'DragEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ drop: {
+ EventType: 'DragEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ mouseDown: {
+ EventType: 'MouseEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ mouseEnter: {
+ EventType: 'MouseEvent',
+ defaultInit: {bubbles: false, cancelable: false, composed: true},
+ },
+ mouseLeave: {
+ EventType: 'MouseEvent',
+ defaultInit: {bubbles: false, cancelable: false, composed: true},
+ },
+ mouseMove: {
+ EventType: 'MouseEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ mouseOut: {
+ EventType: 'MouseEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ mouseOver: {
+ EventType: 'MouseEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ mouseUp: {
+ EventType: 'MouseEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ // Selection Events
+ select: {
+ EventType: 'Event',
+ defaultInit: {bubbles: true, cancelable: false},
+ },
+ // Touch Events
+ touchCancel: {
+ EventType: 'TouchEvent',
+ defaultInit: {bubbles: true, cancelable: false, composed: true},
+ },
+ touchEnd: {
+ EventType: 'TouchEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ touchMove: {
+ EventType: 'TouchEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ touchStart: {
+ EventType: 'TouchEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ // UI Events
+ scroll: {
+ EventType: 'UIEvent',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ // Wheel Events
+ wheel: {
+ EventType: 'WheelEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ // Media Events
+ abort: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ canPlay: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ canPlayThrough: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ durationChange: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ emptied: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ encrypted: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ ended: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ loadedData: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ loadedMetadata: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ loadStart: {
+ EventType: 'ProgressEvent',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ pause: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ play: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ playing: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ progress: {
+ EventType: 'ProgressEvent',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ rateChange: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ seeked: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ seeking: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ stalled: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ suspend: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ timeUpdate: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ volumeChange: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ waiting: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ // Image Events
+ load: {
+ EventType: 'UIEvent',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ error: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ // Animation Events
+ animationStart: {
+ EventType: 'AnimationEvent',
+ defaultInit: {bubbles: true, cancelable: false},
+ },
+ animationEnd: {
+ EventType: 'AnimationEvent',
+ defaultInit: {bubbles: true, cancelable: false},
+ },
+ animationIteration: {
+ EventType: 'AnimationEvent',
+ defaultInit: {bubbles: true, cancelable: false},
+ },
+ // Transition Events
+ transitionEnd: {
+ EventType: 'TransitionEvent',
+ defaultInit: {bubbles: true, cancelable: true},
+ },
+ // pointer events
+ pointerOver: {
+ EventType: 'PointerEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true, button: -1},
+ },
+ pointerEnter: {
+ EventType: 'PointerEvent',
+ defaultInit: {bubbles: false, cancelable: false, button: -1},
+ },
+ pointerDown: {
+ EventType: 'PointerEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true, button: -1},
+ },
+ pointerMove: {
+ EventType: 'PointerEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true, button: -1},
+ },
+ pointerUp: {
+ EventType: 'PointerEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true, button: -1},
+ },
+ pointerCancel: {
+ EventType: 'PointerEvent',
+ defaultInit: {bubbles: true, cancelable: false, composed: true, button: -1},
+ },
+ pointerOut: {
+ EventType: 'PointerEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true, button: -1},
+ },
+ pointerLeave: {
+ EventType: 'PointerEvent',
+ defaultInit: {bubbles: false, cancelable: false, button: -1},
+ },
+ gotPointerCapture: {
+ EventType: 'PointerEvent',
+ defaultInit: {
+ bubbles: false,
+ cancelable: false,
+ composed: true,
+ button: -1,
+ },
+ },
+ lostPointerCapture: {
+ EventType: 'PointerEvent',
+ defaultInit: {
+ bubbles: false,
+ cancelable: false,
+ composed: true,
+ button: -1,
+ },
+ },
+ // history events
+ popState: {
+ EventType: 'PopStateEvent',
+ defaultInit: {bubbles: true, cancelable: false},
+ },
+}
+
+export const eventAliasMap = {
+ doubleClick: 'dblClick',
+}
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/src/index.js b/src/index.js
index 2a279383..6a2fb404 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,6 +1,7 @@
import {getQueriesForElement} from './get-queries-for-element'
import * as queries from './queries'
import * as queryHelpers from './query-helpers'
+import * as userEvent from './user-event'
export * from './queries'
export * from './wait-for'
@@ -26,4 +27,5 @@ export {
// export query utils under a namespace for convenience:
queries,
queryHelpers,
+ userEvent,
}
diff --git a/src/user-event/.eslintrc b/src/user-event/.eslintrc
new file mode 100644
index 00000000..b7d78581
--- /dev/null
+++ b/src/user-event/.eslintrc
@@ -0,0 +1,8 @@
+{
+ "rules": {
+ // everything in this directory is intentionally running in series, not parallel
+ // because user's cannot fire multiple events at the same time and we need
+ // all events fired in a predictable order.
+ "no-await-in-loop": "off"
+ }
+}
diff --git a/src/user-event/__mocks__/utils.js b/src/user-event/__mocks__/utils.js
new file mode 100644
index 00000000..30ff2456
--- /dev/null
+++ b/src/user-event/__mocks__/utils.js
@@ -0,0 +1,50 @@
+// this helps us track what the state is before and after an event is fired
+// this is needed for determining the snapshot values
+const actual = jest.requireActual('../utils')
+
+function getTrackedElementValues(element) {
+ return {
+ value: element.value,
+ checked: element.checked,
+ selectionStart: element.selectionStart,
+ selectionEnd: element.selectionEnd,
+
+ // unfortunately, changing a select option doesn't happen within fireEvent
+ // but rather imperatively via `options.selected = newValue`
+ // because of this we don't (currently) have a way to track before/after
+ // in a given fireEvent call.
+ }
+}
+
+function wrapWithTestData(fn) {
+ return async (element, init) => {
+ const before = getTrackedElementValues(element)
+ const testData = {before}
+
+ // put it on the element so the event handler can grab it
+ element.testData = testData
+ const result = await fn(element, init)
+
+ const after = getTrackedElementValues(element)
+ Object.assign(testData, {after})
+
+ // elete the testData for the next event
+ delete element.testData
+ return result
+ }
+}
+
+const mockFireEvent = wrapWithTestData(actual.fireEvent)
+
+for (const key of Object.keys(actual.fireEvent)) {
+ if (typeof actual.fireEvent[key] === 'function') {
+ mockFireEvent[key] = wrapWithTestData(actual.fireEvent[key], key)
+ } else {
+ mockFireEvent[key] = actual.fireEvent[key]
+ }
+}
+
+module.exports = {
+ ...actual,
+ fireEvent: mockFireEvent,
+}
diff --git a/src/user-event/__tests__/blur.js b/src/user-event/__tests__/blur.js
new file mode 100644
index 00000000..197b036c
--- /dev/null
+++ b/src/user-event/__tests__/blur.js
@@ -0,0 +1,65 @@
+import {userEvent} from '../../'
+import {setup} from './helpers/utils'
+
+test('blur a button', async () => {
+ const {element, getEventSnapshot, clearEventCalls} = setup(``)
+ await userEvent.focus(element)
+ clearEventCalls()
+ await userEvent.blur(element)
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
+ Events fired on: button
+
+ button - blur
+ button - focusout
+ `)
+ expect(element).not.toHaveFocus()
+})
+
+test('no events fired on an unblurable input', async () => {
+ const {element, getEventSnapshot, clearEventCalls} = setup(`
`)
+ await userEvent.focus(element)
+ clearEventCalls()
+ await userEvent.blur(element)
+ expect(getEventSnapshot()).toMatchInlineSnapshot(
+ `No events were fired on: div`,
+ )
+ expect(element).not.toHaveFocus()
+})
+
+test('blur with tabindex', async () => {
+ const {element, getEventSnapshot, clearEventCalls} = setup(
+ ``,
+ )
+ await userEvent.focus(element)
+ clearEventCalls()
+ await userEvent.blur(element)
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
+ Events fired on: div
+
+ div - blur
+ div - focusout
+ `)
+ expect(element).not.toHaveFocus()
+})
+
+test('no events fired on a disabled blurable input', async () => {
+ const {element, getEventSnapshot, clearEventCalls} = setup(
+ ``,
+ )
+ await userEvent.focus(element)
+ clearEventCalls()
+ await userEvent.blur(element)
+ expect(getEventSnapshot()).toMatchInlineSnapshot(
+ `No events were fired on: button`,
+ )
+ expect(element).not.toHaveFocus()
+})
+
+test('no events fired if the element is not focused', async () => {
+ const {element, getEventSnapshot} = setup(``)
+ await userEvent.blur(element)
+ expect(getEventSnapshot()).toMatchInlineSnapshot(
+ `No events were fired on: button`,
+ )
+ expect(element).not.toHaveFocus()
+})
diff --git a/src/user-event/__tests__/clear.js b/src/user-event/__tests__/clear.js
new file mode 100644
index 00000000..c6706cb1
--- /dev/null
+++ b/src/user-event/__tests__/clear.js
@@ -0,0 +1,94 @@
+import {userEvent} from '../../'
+import {setup} from './helpers/utils'
+
+test('clears text', async () => {
+ const {element, getEventSnapshot} = setup('')
+ await userEvent.clear(element)
+ expect(element).toHaveValue('')
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
+ Events fired on: input[value=""]
+
+ input[value="hello"] - pointerover
+ input[value="hello"] - pointerenter
+ input[value="hello"] - mouseover: Left (0)
+ input[value="hello"] - mouseenter: Left (0)
+ input[value="hello"] - pointermove
+ input[value="hello"] - mousemove: Left (0)
+ input[value="hello"] - pointerdown
+ input[value="hello"] - mousedown: Left (0)
+ input[value="hello"] - focus
+ input[value="hello"] - focusin
+ input[value="hello"] - pointerup
+ input[value="hello"] - mouseup: Left (0)
+ input[value="hello"] - click: Left (0)
+ input[value="hello"] - select
+ input[value="hello"] - keydown: Delete (46)
+ input[value=""] - input
+ "{SELECTION}hello{/SELECTION}" -> "{CURSOR}"
+ input[value=""] - keyup: Delete (46)
+ `)
+})
+
+test('works with textarea', async () => {
+ const {element} = setup('')
+ await userEvent.clear(element)
+ expect(element).toHaveValue('')
+})
+
+test('does not clear text on disabled inputs', async () => {
+ const {element, getEventSnapshot} = setup('')
+ await userEvent.clear(element)
+ expect(element).toHaveValue('hello')
+ expect(getEventSnapshot()).toMatchInlineSnapshot(
+ `No events were fired on: input[value="hello"]`,
+ )
+})
+
+test('does not clear text on readonly inputs', async () => {
+ const {element, getEventSnapshot} = setup('')
+ await userEvent.clear(element)
+ expect(element).toHaveValue('hello')
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
+ Events fired on: input[value="hello"]
+
+ input[value="hello"] - pointerover
+ input[value="hello"] - pointerenter
+ input[value="hello"] - mouseover: Left (0)
+ input[value="hello"] - mouseenter: Left (0)
+ input[value="hello"] - pointermove
+ input[value="hello"] - mousemove: Left (0)
+ input[value="hello"] - pointerdown
+ input[value="hello"] - mousedown: Left (0)
+ input[value="hello"] - focus
+ input[value="hello"] - focusin
+ input[value="hello"] - pointerup
+ input[value="hello"] - mouseup: Left (0)
+ input[value="hello"] - click: Left (0)
+ input[value="hello"] - select
+ input[value="hello"] - keydown: Delete (46)
+ input[value="hello"] - keyup: Delete (46)
+ `)
+})
+
+test('clears even on inputs that cannot (programmatically) have a selection', async () => {
+ const {element: email} = setup('')
+ await userEvent.clear(email)
+ expect(email).toHaveValue('')
+
+ const {element: password} = setup('')
+ await userEvent.clear(password)
+ expect(password).toHaveValue('')
+
+ const {element: number} = setup('')
+ await userEvent.clear(number)
+ // jest-dom does funny stuff with toHaveValue on number inputs
+ expect(number.value).toBe('')
+})
+
+test('non-inputs/textareas are currently unsupported', async () => {
+ const {element} = setup('')
+ const error = await userEvent.clear(element).catch(e => e)
+ expect(error).toMatchInlineSnapshot(
+ `[Error: clear currently only supports input and textarea elements.]`,
+ )
+})
diff --git a/src/user-event/__tests__/click.js b/src/user-event/__tests__/click.js
new file mode 100644
index 00000000..a35573d8
--- /dev/null
+++ b/src/user-event/__tests__/click.js
@@ -0,0 +1,421 @@
+import {userEvent} from '../../'
+import {setup, addEventListener, addListeners} from './helpers/utils'
+
+test('click in button', async () => {
+ const {element, getEventSnapshot} = setup('')
+ await userEvent.click(element)
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
+ Events fired on: button
+
+ button - pointerover
+ button - pointerenter
+ button - mouseover: Left (0)
+ button - mouseenter: Left (0)
+ button - pointermove
+ button - mousemove: Left (0)
+ button - pointerdown
+ button - mousedown: Left (0)
+ button - focus
+ button - focusin
+ button - pointerup
+ button - mouseup: Left (0)
+ button - click: Left (0)
+ `)
+})
+
+test('only fires pointer events when clicking a disabled button', async () => {
+ const {element, getEventSnapshot} = setup('')
+ await userEvent.click(element)
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
+ Events fired on: button
+
+ button - pointerover
+ button - pointerenter
+ button - pointermove
+ button - pointerdown
+ button - pointerup
+ `)
+})
+
+test('clicking a checkbox', async () => {
+ const {element, getEventSnapshot} = setup('')
+ expect(element).not.toBeChecked()
+ await userEvent.click(element)
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
+ Events fired on: input[checked=true]
+
+ input[checked=false] - pointerover
+ input[checked=false] - pointerenter
+ input[checked=false] - mouseover: Left (0)
+ input[checked=false] - mouseenter: Left (0)
+ input[checked=false] - pointermove
+ input[checked=false] - mousemove: Left (0)
+ input[checked=false] - pointerdown
+ input[checked=false] - mousedown: Left (0)
+ input[checked=false] - focus
+ input[checked=false] - focusin
+ input[checked=false] - pointerup
+ input[checked=false] - mouseup: Left (0)
+ input[checked=true] - click: Left (0)
+ unchecked -> checked
+ input[checked=true] - input
+ input[checked=true] - change
+ `)
+})
+
+test('clicking a disabled checkbox only fires pointer events', async () => {
+ const {element, getEventSnapshot} = setup(
+ '',
+ )
+ await userEvent.click(element)
+ expect(element).toBeDisabled()
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
+ Events fired on: input[checked=false]
+
+ input[checked=false] - pointerover
+ input[checked=false] - pointerenter
+ input[checked=false] - pointermove
+ input[checked=false] - pointerdown
+ input[checked=false] - pointerup
+ `)
+ expect(element).toBeDisabled()
+ expect(element).toHaveProperty('checked', false)
+})
+
+test('clicking a radio button', async () => {
+ const {element, getEventSnapshot} = setup('')
+ expect(element).not.toBeChecked()
+ await userEvent.click(element)
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
+ Events fired on: input[checked=true]
+
+ input[checked=false] - pointerover
+ input[checked=false] - pointerenter
+ input[checked=false] - mouseover: Left (0)
+ input[checked=false] - mouseenter: Left (0)
+ input[checked=false] - pointermove
+ input[checked=false] - mousemove: Left (0)
+ input[checked=false] - pointerdown
+ input[checked=false] - mousedown: Left (0)
+ input[checked=false] - focus
+ input[checked=false] - focusin
+ input[checked=false] - pointerup
+ input[checked=false] - mouseup: Left (0)
+ input[checked=true] - click: Left (0)
+ unchecked -> checked
+ input[checked=true] - input
+ input[checked=true] - change
+ `)
+
+ expect(element).toHaveProperty('checked', true)
+})
+
+test('clicking a disabled radio button only fires pointer events', async () => {
+ const {element, getEventSnapshot} = setup('')
+ await userEvent.click(element)
+ expect(element).toBeDisabled()
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
+ Events fired on: input[checked=false]
+
+ input[checked=false] - pointerover
+ input[checked=false] - pointerenter
+ input[checked=false] - pointermove
+ input[checked=false] - pointerdown
+ input[checked=false] - pointerup
+ `)
+ expect(element).toBeDisabled()
+
+ expect(element).toHaveProperty('checked', false)
+})
+
+test('should fire the correct events for ', async () => {
+ const {element, getEventSnapshot} = setup('
')
+ await userEvent.click(element)
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
+ Events fired on: div
+
+ div - pointerover
+ div - pointerenter
+ div - mouseover: Left (0)
+ div - mouseenter: Left (0)
+ div - pointermove
+ div - mousemove: Left (0)
+ div - pointerdown
+ div - mousedown: Left (0)
+ div - pointerup
+ div - mouseup: Left (0)
+ div - click: Left (0)
+ `)
+})
+
+test('toggles the focus', async () => {
+ const {element} = setup(`
`)
+
+ const a = element.children[0]
+ const b = element.children[1]
+
+ expect(a).not.toHaveFocus()
+ expect(b).not.toHaveFocus()
+
+ await userEvent.click(a)
+ expect(a).toHaveFocus()
+ expect(b).not.toHaveFocus()
+
+ await userEvent.click(b)
+ expect(a).not.toHaveFocus()
+ expect(b).toHaveFocus()
+})
+
+test('should blur the previous element', async () => {
+ const {element, getEventSnapshot, clearEventCalls} = setup(
+ `
`,
+ )
+
+ const a = element.children[0]
+ const b = element.children[1]
+
+ const aListeners = addListeners(a)
+ const bListeners = addListeners(b)
+
+ await userEvent.click(a)
+ clearEventCalls()
+ await userEvent.click(b)
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
+ Events fired on: div
+
+ input[name="b"][value=""] - pointerover
+ input[name="b"][value=""] - mouseover: Left (0)
+ input[name="b"][value=""] - pointermove
+ input[name="b"][value=""] - mousemove: Left (0)
+ input[name="b"][value=""] - pointerdown
+ input[name="b"][value=""] - mousedown: Left (0)
+ input[name="a"][value=""] - focusout
+ input[name="b"][value=""] - focusin
+ input[name="b"][value=""] - pointerup
+ input[name="b"][value=""] - mouseup: Left (0)
+ input[name="b"][value=""] - click: Left (0)
+ `)
+ // focus/blur events don't bubble (but the focusout/focusin do!)
+ // we just want to make sure the blur was fired on a
+ // and the focus was fired on b
+ expect(aListeners.eventWasFired('blur')).toBe(true)
+ expect(bListeners.eventWasFired('focus')).toBe(true)
+})
+
+test('should not blur the previous element when mousedown prevents default', async () => {
+ const {element, getEventSnapshot, clearEventCalls} = setup(
+ `
`,
+ )
+
+ const a = element.children[0]
+ const b = element.children[1]
+
+ const aListeners = addListeners(a)
+ const bListeners = addListeners(b, {
+ eventHandlers: {mouseDown: e => e.preventDefault()},
+ })
+
+ await userEvent.click(a)
+ clearEventCalls()
+ await userEvent.click(b)
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
+ Events fired on: div
+
+ input[name="b"][value=""] - pointerover
+ input[name="b"][value=""] - mouseover: Left (0)
+ input[name="b"][value=""] - pointermove
+ input[name="b"][value=""] - mousemove: Left (0)
+ input[name="b"][value=""] - pointerdown
+ input[name="b"][value=""] - mousedown: Left (0)
+ input[name="b"][value=""] - pointerup
+ input[name="b"][value=""] - mouseup: Left (0)
+ input[name="b"][value=""] - click: Left (0)
+ `)
+ // focus/blur events don't bubble (but the focusout do!)
+ // we just want to make sure the blur was fired on a
+ // and the focus was fired on b
+ expect(aListeners.eventWasFired('blur')).toBe(false)
+ expect(bListeners.eventWasFired('focus')).toBe(false)
+})
+
+test('does not lose focus when click updates focus', async () => {
+ const {element} = setup(`
`)
+ const input = element.children[0]
+ const button = element.children[1]
+
+ addEventListener(button, 'click', () => input.focus())
+
+ expect(input).not.toHaveFocus()
+
+ await userEvent.click(button)
+ expect(input).toHaveFocus()
+
+ await userEvent.click(button)
+ expect(input).toHaveFocus()
+})
+
+test('gives focus to the form control when clicking the label', async () => {
+ const {element} = setup(`
+
+
+
+
+ `)
+ const label = element.children[0]
+ const input = element.children[1]
+
+ await userEvent.click(label)
+ expect(input).toHaveFocus()
+})
+
+test('gives focus to the form control when clicking within a label', async () => {
+ const {element} = setup(`
+
+
+
+
+ `)
+ const label = element.children[0]
+ const span = label.firstChild
+ const input = element.children[1]
+
+ await userEvent.click(span)
+ expect(input).toHaveFocus()
+})
+
+test('fires no events when clicking a label with a nested control that is disabled', async () => {
+ const {element, getEventSnapshot} = setup(`
`)
+ await userEvent.click(element)
+ expect(getEventSnapshot()).toMatchInlineSnapshot(
+ `No events were fired on: label`,
+ )
+})
+
+test('does not crash if the label has no control', async () => {
+ const {element} = setup(`
`)
+ await userEvent.click(element)
+})
+
+test('clicking a label checks the checkbox', async () => {
+ const {element} = setup(`
+
+
+
+
+ `)
+ const label = element.children[0]
+ const input = element.children[1]
+
+ await userEvent.click(label)
+ expect(input).toHaveFocus()
+ expect(input).toBeChecked()
+})
+
+test('clicking a label checks the radio', async () => {
+ const {element} = setup(`
+
+
+
+
+ `)
+ const label = element.children[0]
+ const input = element.children[1]
+
+ await userEvent.click(label)
+ expect(input).toHaveFocus()
+ expect(input).toBeChecked()
+})
+
+test('submits a form when clicking on a