src/ngMock/browserTrigger.js
'use strict';
(function() {
/**
* @ngdoc function
* @name browserTrigger
* @description
*
* This is a global (window) function that is only available when the {@link ngMock} module is
* included.
*
* It can be used to trigger a native browser event on an element, which is useful for unit testing.
*
*
* @param {Object} element Either a wrapped jQuery/jqLite node or a DOMElement
* @param {string=} eventType Optional event type. If none is specified, the function tries
* to determine the right event type for the element, e.g. `change` for
* `input[text]`.
* @param {Object=} eventData An optional object which contains additional event data that is used
* when creating the event:
*
* - `bubbles`: [Event.bubbles](https://developer.mozilla.org/docs/Web/API/Event/bubbles).
* Not applicable to all events.
*
* - `cancelable`: [Event.cancelable](https://developer.mozilla.org/docs/Web/API/Event/cancelable).
* Not applicable to all events.
*
* - `charcode`: [charCode](https://developer.mozilla.org/docs/Web/API/KeyboardEvent/charcode)
* for keyboard events (keydown, keypress, and keyup).
*
* - `data`: [data](https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent/data) for
* [CompositionEvents](https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent).
*
* - `elapsedTime`: the elapsedTime for
* [TransitionEvent](https://developer.mozilla.org/docs/Web/API/TransitionEvent)
* and [AnimationEvent](https://developer.mozilla.org/docs/Web/API/AnimationEvent).
*
* - `keycode`: [keyCode](https://developer.mozilla.org/docs/Web/API/KeyboardEvent/keycode)
* for keyboard events (keydown, keypress, and keyup).
*
* - `keys`: an array of possible modifier keys (ctrl, alt, shift, meta) for
* [MouseEvent](https://developer.mozilla.org/docs/Web/API/MouseEvent) and
* keyboard events (keydown, keypress, and keyup).
*
* - `relatedTarget`: the
* [relatedTarget](https://developer.mozilla.org/docs/Web/API/MouseEvent/relatedTarget)
* for [MouseEvent](https://developer.mozilla.org/docs/Web/API/MouseEvent).
*
* - `which`: [which](https://developer.mozilla.org/docs/Web/API/KeyboardEvent/which)
* for keyboard events (keydown, keypress, and keyup).
*
* - `x`: x-coordinates for [MouseEvent](https://developer.mozilla.org/docs/Web/API/MouseEvent)
* and [TouchEvent](https://developer.mozilla.org/docs/Web/API/TouchEvent).
*
* - `y`: y-coordinates for [MouseEvent](https://developer.mozilla.org/docs/Web/API/MouseEvent)
* and [TouchEvent](https://developer.mozilla.org/docs/Web/API/TouchEvent).
*
*/
window.browserTrigger = function browserTrigger(element, eventType, eventData) {
if (element && !element.nodeName) element = element[0];
if (!element) return;
eventData = eventData || {};
var relatedTarget = eventData.relatedTarget || element;
var keys = eventData.keys;
var x = eventData.x;
var y = eventData.y;
var inputType = (element.type) ? element.type.toLowerCase() : null,
nodeName = element.nodeName.toLowerCase();
if (!eventType) {
eventType = {
'text': 'change',
'textarea': 'change',
'hidden': 'change',
'password': 'change',
'button': 'click',
'submit': 'click',
'reset': 'click',
'image': 'click',
'checkbox': 'click',
'radio': 'click',
'select-one': 'change',
'select-multiple': 'change',
'_default_': 'click'
}[inputType || '_default_'];
}
if (nodeName === 'option') {
element.parentNode.value = element.value;
element = element.parentNode;
eventType = 'change';
}
keys = keys || [];
function pressed(key) {
return keys.indexOf(key) !== -1;
}
var evnt;
if (/transitionend/.test(eventType)) {
if (window.WebKitTransitionEvent) {
evnt = new window.WebKitTransitionEvent(eventType, eventData);
evnt.initEvent(eventType, eventData.bubbles, true);
} else {
try {
evnt = new window.TransitionEvent(eventType, eventData);
} catch (e) {
evnt = window.document.createEvent('TransitionEvent');
evnt.initTransitionEvent(eventType, eventData.bubbles, null, null, eventData.elapsedTime || 0);
}
}
} else if (/animationend/.test(eventType)) {
if (window.WebKitAnimationEvent) {
evnt = new window.WebKitAnimationEvent(eventType, eventData);
evnt.initEvent(eventType, eventData.bubbles, true);
} else {
try {
evnt = new window.AnimationEvent(eventType, eventData);
} catch (e) {
evnt = window.document.createEvent('AnimationEvent');
evnt.initAnimationEvent(eventType, eventData.bubbles, null, null, eventData.elapsedTime || 0);
}
}
} else if (/touch/.test(eventType) && supportsTouchEvents()) {
evnt = createTouchEvent(element, eventType, x, y);
} else if (/key/.test(eventType)) {
evnt = window.document.createEvent('Events');
evnt.initEvent(eventType, eventData.bubbles, eventData.cancelable);
evnt.view = window;
evnt.ctrlKey = pressed('ctrl');
evnt.altKey = pressed('alt');
evnt.shiftKey = pressed('shift');
evnt.metaKey = pressed('meta');
evnt.keyCode = eventData.keyCode;
evnt.charCode = eventData.charCode;
evnt.which = eventData.which;
} else if (/composition/.test(eventType)) {
try {
evnt = new window.CompositionEvent(eventType, {
data: eventData.data
});
} catch (e) {
// Support: IE9+
evnt = window.document.createEvent('CompositionEvent', {});
evnt.initCompositionEvent(
eventType,
eventData.bubbles,
eventData.cancelable,
window,
eventData.data,
null
);
}
} else {
evnt = window.document.createEvent('MouseEvents');
x = x || 0;
y = y || 0;
evnt.initMouseEvent(eventType, true, true, window, 0, x, y, x, y, pressed('ctrl'),
pressed('alt'), pressed('shift'), pressed('meta'), 0, relatedTarget);
}
/* we're unable to change the timeStamp value directly so this
* is only here to allow for testing where the timeStamp value is
* read */
evnt.$manualTimeStamp = eventData.timeStamp;
if (!evnt) return;
if (!eventData.bubbles || supportsEventBubblingInDetachedTree() || isAttachedToDocument(element)) {
return element.dispatchEvent(evnt);
} else {
triggerForPath(element, evnt);
}
};
function supportsTouchEvents() {
if ('_cached' in supportsTouchEvents) {
return supportsTouchEvents._cached;
}
if (!window.document.createTouch || !window.document.createTouchList) {
supportsTouchEvents._cached = false;
return false;
}
try {
window.document.createEvent('TouchEvent');
} catch (e) {
supportsTouchEvents._cached = false;
return false;
}
supportsTouchEvents._cached = true;
return true;
}
function createTouchEvent(element, eventType, x, y) {
var evnt = new window.Event(eventType);
x = x || 0;
y = y || 0;
var touch = window.document.createTouch(window, element, Date.now(), x, y, x, y);
var touches = window.document.createTouchList(touch);
evnt.touches = touches;
return evnt;
}
function supportsEventBubblingInDetachedTree() {
if ('_cached' in supportsEventBubblingInDetachedTree) {
return supportsEventBubblingInDetachedTree._cached;
}
supportsEventBubblingInDetachedTree._cached = false;
var doc = window.document;
if (doc) {
var parent = doc.createElement('div'),
child = parent.cloneNode();
parent.appendChild(child);
parent.addEventListener('e', function() {
supportsEventBubblingInDetachedTree._cached = true;
});
var evnt = window.document.createEvent('Events');
evnt.initEvent('e', true, true);
child.dispatchEvent(evnt);
}
return supportsEventBubblingInDetachedTree._cached;
}
function triggerForPath(element, evnt) {
var stop = false;
var _stopPropagation = evnt.stopPropagation;
evnt.stopPropagation = function() {
stop = true;
_stopPropagation.apply(evnt, arguments);
};
patchEventTargetForBubbling(evnt, element);
do {
element.dispatchEvent(evnt);
// eslint-disable-next-line no-unmodified-loop-condition
} while (!stop && (element = element.parentNode));
}
function patchEventTargetForBubbling(event, target) {
event._target = target;
Object.defineProperty(event, 'target', {get: function() { return this._target;}});
}
function isAttachedToDocument(element) {
while ((element = element.parentNode)) {
if (element === window) {
return true;
}
}
return false;
}
})();