@ember/test-helpers
Version:
Helpers for testing Ember.js applications
232 lines (219 loc) • 7.83 kB
JavaScript
import { isDocument, isElement } from './-target.js';
import tuple from '../-tuple.js';
import { log } from './-logging.js';
import { registerHook, runHooks } from '../helper-hooks.js';
registerHook('fireEvent', 'start', target => {
log('fireEvent', target);
});
// eslint-disable-next-line require-jsdoc
const MOUSE_EVENT_CONSTRUCTOR = (() => {
try {
new MouseEvent('test');
return true;
} catch {
return false;
}
})();
const DEFAULT_EVENT_OPTIONS = {
bubbles: true,
cancelable: true
};
const KEYBOARD_EVENT_TYPES = tuple('keydown', 'keypress', 'keyup');
// eslint-disable-next-line require-jsdoc
function isKeyboardEventType(eventType) {
return KEYBOARD_EVENT_TYPES.indexOf(eventType) > -1;
}
const MOUSE_EVENT_TYPES = tuple('click', 'mousedown', 'mouseup', 'dblclick', 'mouseenter', 'mouseleave', 'mousemove', 'mouseout', 'mouseover');
// eslint-disable-next-line require-jsdoc
function isMouseEventType(eventType) {
return MOUSE_EVENT_TYPES.indexOf(eventType) > -1;
}
const FILE_SELECTION_EVENT_TYPES = tuple('change');
// eslint-disable-next-line require-jsdoc
function isFileSelectionEventType(eventType) {
return FILE_SELECTION_EVENT_TYPES.indexOf(eventType) > -1;
}
// eslint-disable-next-line require-jsdoc
function isFileSelectionInput(element) {
return element.files;
}
/**
Internal helper used to build and dispatch events throughout the other DOM helpers.
@private
@param {Element} element the element to dispatch the event to
@param {string} eventType the type of event
@param {Object} [options] additional properties to be set on the event
@returns {Event} the event that was dispatched
*/
function fireEvent(element, eventType, options = {}) {
return Promise.resolve().then(() => runHooks('fireEvent', 'start', element)).then(() => runHooks(`fireEvent:${eventType}`, 'start', element)).then(() => {
if (!element) {
throw new Error('Must pass an element to `fireEvent`');
}
let event;
if (isKeyboardEventType(eventType)) {
event = _buildKeyboardEvent(eventType, options);
} else if (isMouseEventType(eventType)) {
let rect;
if (element instanceof Window && element.document.documentElement) {
rect = element.document.documentElement.getBoundingClientRect();
} else if (isDocument(element)) {
rect = element.documentElement.getBoundingClientRect();
} else if (isElement(element)) {
rect = element.getBoundingClientRect();
} else {
return;
}
const x = rect.left + 1;
const y = rect.top + 1;
const simulatedCoordinates = {
screenX: x + 5,
// Those numbers don't really mean anything.
screenY: y + 95,
// They're just to make the screenX/Y be different of clientX/Y..
clientX: x,
clientY: y,
...options
};
event = buildMouseEvent(eventType, simulatedCoordinates);
} else if (isFileSelectionEventType(eventType) && isFileSelectionInput(element)) {
event = buildFileEvent(eventType, element, options);
} else {
event = buildBasicEvent(eventType, options);
}
element.dispatchEvent(event);
return event;
}).then(event => runHooks(`fireEvent:${eventType}`, 'end', element).then(() => event)).then(event => runHooks('fireEvent', 'end', element).then(() => event));
}
// eslint-disable-next-line require-jsdoc
function buildBasicEvent(type, options = {}) {
const event = document.createEvent('Events');
const bubbles = options.bubbles !== undefined ? options.bubbles : true;
const cancelable = options.cancelable !== undefined ? options.cancelable : true;
delete options.bubbles;
delete options.cancelable;
// bubbles and cancelable are readonly, so they can be
// set when initializing event
event.initEvent(type, bubbles, cancelable);
for (const prop in options) {
event[prop] = options[prop];
}
return event;
}
// eslint-disable-next-line require-jsdoc
function buildMouseEvent(type, options = {}) {
let event;
const eventOpts = {
view: window,
...DEFAULT_EVENT_OPTIONS,
...options
};
if (MOUSE_EVENT_CONSTRUCTOR) {
event = new MouseEvent(type, eventOpts);
} else {
try {
event = document.createEvent('MouseEvents');
event.initMouseEvent(type, eventOpts.bubbles, eventOpts.cancelable, window, eventOpts.detail, eventOpts.screenX, eventOpts.screenY, eventOpts.clientX, eventOpts.clientY, eventOpts.ctrlKey, eventOpts.altKey, eventOpts.shiftKey, eventOpts.metaKey, eventOpts.button, eventOpts.relatedTarget);
} catch {
event = buildBasicEvent(type, options);
}
}
return event;
}
// @private
// eslint-disable-next-line require-jsdoc
function _buildKeyboardEvent(type, options = {}) {
const eventOpts = {
...DEFAULT_EVENT_OPTIONS,
...options
};
let event;
let eventMethodName;
try {
event = new KeyboardEvent(type, eventOpts);
// Property definitions are required for B/C for keyboard event usage
// If this properties are not defined, when listening for key events
// keyCode/which will be 0. Also, keyCode and which now are string
// and if app compare it with === with integer key definitions,
// there will be a fail.
//
// https://w3c.github.io/uievents/#interface-keyboardevent
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
Object.defineProperty(event, 'keyCode', {
get() {
return parseInt(eventOpts.keyCode);
}
});
Object.defineProperty(event, 'which', {
get() {
return parseInt(eventOpts.which);
}
});
return event;
} catch {
// left intentionally blank
}
try {
event = document.createEvent('KeyboardEvents');
eventMethodName = 'initKeyboardEvent';
} catch {
// left intentionally blank
}
if (!event) {
try {
event = document.createEvent('KeyEvents');
eventMethodName = 'initKeyEvent';
} catch {
// left intentionally blank
}
}
if (event && eventMethodName) {
event[eventMethodName](type, eventOpts.bubbles, eventOpts.cancelable, window, eventOpts.ctrlKey, eventOpts.altKey, eventOpts.shiftKey, eventOpts.metaKey, eventOpts.keyCode, eventOpts.charCode);
} else {
event = buildBasicEvent(type, options);
}
return event;
}
// eslint-disable-next-line require-jsdoc
function buildFileEvent(type, element, options = {}) {
const event = buildBasicEvent(type);
const files = options.files;
if (Array.isArray(options)) {
throw new Error('Please pass an object with a files array to `triggerEvent` instead of passing the `options` param as an array to.');
}
if (Array.isArray(files)) {
Object.defineProperty(files, 'item', {
value(index) {
return typeof index === 'number' ? this[index] : null;
},
configurable: true
});
Object.defineProperty(element, 'files', {
value: files,
configurable: true
});
const elementProto = Object.getPrototypeOf(element);
const valueProp = Object.getOwnPropertyDescriptor(elementProto, 'value');
Object.defineProperty(element, 'value', {
configurable: true,
get() {
return valueProp.get.call(element);
},
set(value) {
valueProp.set.call(element, value);
// We are sure that the value is empty here.
// For a non-empty value the original setter must raise an exception.
Object.defineProperty(element, 'files', {
configurable: true,
value: []
});
}
});
}
Object.defineProperty(event, 'target', {
value: element
});
return event;
}
export { KEYBOARD_EVENT_TYPES, _buildKeyboardEvent, fireEvent as default, isFileSelectionEventType, isFileSelectionInput, isKeyboardEventType, isMouseEventType };
//# sourceMappingURL=fire-event.js.map