siesta-lite
Version:
Stress-free JavaScript unit testing and functional testing tool, works in NodeJS and browsers
720 lines (557 loc) • 29.9 kB
HTML
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>The source code</title>
<link href="../resources/prettify/prettify.css" type="text/css" rel="stylesheet" />
<script type="text/javascript" src="../resources/prettify/prettify.js"></script>
<style type="text/css">
.highlight { display: block; background-color: #ddd; }
</style>
<script type="text/javascript">
function highlight() {
document.getElementById(location.hash.replace(/#/, "")).className = "highlight";
}
</script>
</head>
<body onload="prettyPrint(); highlight();">
<pre class="prettyprint lang-js">/*
Siesta 5.6.1
Copyright(c) 2009-2022 Bryntum AB
https://bryntum.com/contact
https://bryntum.com/products/siesta/license
*/
Role('Siesta.Test.Simulate.Keyboard', {
requires : [
'$',
'simulateEvent', 'isEventPrevented'
/*'getSimulateEventsWith', 'getElementAtCursor'*/
],
does : [
Siesta.Util.Role.CanFormatStrings,
Siesta.Test.Browser.Role.CanWorkWithKeyboard
],
has : {
keyboardEventName : ("KeyboardEvent" in window && !bowser.msedge) ? "KeyboardEvent" : ("KeyEvent" in window ? "KeyEvents" : null)
},
methods: {
// TODO switch fully to KeyboardEvent https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/KeyboardEvent
// private
createKeyboardEvent: function (type, options, el) {
var event;
var doc = el.ownerDocument,
global = this.global;
options = $.extend({
bubbles : true,
cancelable : true,
view : this.global,
ctrlKey : false,
altKey : false,
shiftKey : false,
metaKey : false,
keyCode : 0,
charCode : 0,
// https://developer.mozilla.org/en-US/docs/Web/API/Event/composed
// The read-only composed property of the Event interface returns a Boolean which indicates whether or not
// the event will propagate across the shadow DOM boundary into the standard DOM.
composed : true,
key : options.key || ''
}, options);
// use W3C standard when available and allowed by "simulateEventsWith" option
if (doc.createEvent && this.getSimulateEventsWith() === 'dispatchEvent') {
try {
if (this.keyboardEventName === 'KeyboardEvent') {
event = new this.global.KeyboardEvent(type, options);
}
} catch (err) {
event = null;
}
if (!event) {
event = doc.createEvent("Events");
event.initEvent(type, options.bubbles, options.cancelable);
$.extend(event, options);
}
} else if (doc.createEventObject) {
event = doc.createEventObject();
$.extend(event, options);
}
if (bowser.msie || bowser.opera) {
event.keyCode = (options.charCode > 0) ? options.charCode : options.keyCode;
event.charCode = undefined;
}
return event;
},
// private
createTextEvent: function (type, options, el) {
var doc = el.ownerDocument;
var event = null;
// only for Webkit / IE for now
if (doc.createEvent) {
try {
event = doc.createEvent('TextEvent');
if (event && event.initTextEvent) {
event.initTextEvent(
type,
true,
true,
this.global,
options.text,
// IE ONLY below here
0,
window.navigator.userLanguage || window.navigator.language
);
return event;
}
}
catch(e) {}
}
return null;
},
/*!
* Based on:
*
* @license EmulateTab
* Copyright (c) 2011, 2012 The Swedish Post and Telecom Authority (PTS)
* Developed for PTS by Joel Purra <http://joelpurra.se/>
* Released under the BSD license.
*
* A jQuery plugin to emulate tabbing between elements on a page.
*/
findNextFocusable : function (el, offset) {
var $el = this.$(el)
var $focusable = this.$(":focus, :input, a[href], [tabindex], body", this.getQueryableContainer(el))
.not(":disabled")
.not(":hidden")
.not("a[href]:empty")
var escapeSelectorName = function (str) {
// Based on http://api.jquery.com/category/selectors/
// Still untested
return str.replace(/(!"#$%&'\(\)\*\+,\.\/:;<=>\?@\[\]^`\{\|\}~)/g, "\\\\$1");
}
var isRadio = false
var selector
if (el.tagName === "INPUT" && el.type === "radio" && el.name !== "" ) {
isRadio = true
selector = "input[type=radio][name=" + escapeSelectorName(el.name) + "]"
}
var processed = []
for (var i = 0; i < $focusable.length; i++) {
var currEl = $focusable[ i ]
// always include current element
if (currEl != el && currEl.getAttribute('tabIndex') == -1 || isRadio && $(currEl).is(selector)) continue
processed.push(currEl)
}
var body = this.getBodyElement(el)
var currentTabIndex = Number(el.getAttribute('tabIndex') || 0)
var getTabIndex = function (dom) {
if (dom === el && currentTabIndex === -1) return 0
if (dom === body) return 0
return Number(dom.getAttribute('tabIndex') || 0)
}
processed.sort(function (a, b) {
var aIndex = getTabIndex(a)
var bIndex = getTabIndex(b)
return aIndex < bIndex ? -1 : (aIndex > bIndex ? 1 : (a === body ? 1 : (b === body ? -1 : 0)))
});
var currentIndex = $(processed).index($el);
if (currentIndex === -1) return null
return processed[ (currentIndex + offset) % processed.length ]
},
emulateTab : function (el, offset) {
var next = this.findNextFocusable(el, offset || 1)
if (next)
this.test.focus(next)
else
el.blur()
return next
},
makeSureBlurWorkaroundApplied : function (doc) {
if (doc.__SIESTA_ONBLUR_WORKAROUND_APPLIED__) return
doc.__SIESTA_ONBLUR_WORKAROUND_APPLIED__ = new Siesta.Test.SimulatorOnBlurWorkaround({
document : doc,
simulator : this
})
},
simulateType : function (text, options, params) {
if (text == null) throw 'Must supply a string to type';
var me = this
var el = params.el
this.makeSureBlurWorkaroundApplied(el.ownerDocument)
if (el.disabled) {
return Promise.resolve()
}
// Store initial value of text fields, updated after ENTER key press in ´keyPress´ method
if ('value' in el) {
el.setAttribute('__lastValue', el.value);
} else if (el.isContentEditable) {
// For contentEditable, walk up to find the root editable node
var rootEditableEl = me.closest(el, '[contentEditable]');
if (rootEditableEl && rootEditableEl !== el) {
var range = me.global.document.createRange();
var sel = me.global.getSelection();
try {
range.setStart(el, 1);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
} catch(e) {
// Oh well...
}
}
}
// Extract normal chars, or special keys in brackets such as [TAB], [RIGHT] or [ENTER]
var keys = this.extractKeysAndSpecialKeys(text + '');
var queue = new Siesta.Util.Queue({
deferer : this.test.originalSetTimeout,
deferClearer : this.test.originalClearTimeout,
interval : this.actionDelay,
// this is 0, since user agent `type` method also contains queue with "callbackDelay"
// so we don't need to double that (which also breaks 624_rerun_hotkey)
callbackDelay : 0,
observeTest : this.test,
processor : function (data, index) {
// 1. In IE10, it seems activeElement cannot be trusted as it sometimes returns an empty object with no properties.
// Try to detect this case and simply use the original el
// 2. If user clicks around in the project during ongoing test, the activeElement will be reset to BODY
// If this happens, reuse the original el and hope all is well
var focusedEl = me.activeElement(true, el, el.ownerDocument)
me.simulateKeyPress(focusedEl, data.key, options)
}
})
// the `el` should be already focused in the `type` method of the "user agent" code,
// still allow to focus it, but using special "param.focus"
if (params.focus) {
// Manually focus event to be typed into first
queue.addStep({
processor : function () {
if (!me.nodeIsOrphan(el)) me.focus(el)
}
})
// focus the element one more time for IE - this seems to fix the weird sporadic failures in 042_keyevent_simulation3.t.js
// failures are caused by the field "blur" immediately after 1st focus
// no Ext "focus/blur" methods seems to be called, so it can be a browser behavior
bowser.msie && queue.addStep({
processor : function () {
if (!me.nodeIsOrphan(el)) me.focus(el)
}
})
}
Joose.A.each(keys, function (key, index) {
key = key.length == 1 ? key : key.substring(1, key.length - 1)
keys[ index ] = key
queue.addStep({ key : key })
});
if (!el.readOnly && keys.length) {
var KeyCodes = Siesta.Test.UserAgent.KeyCodes().keys;
var firstKeyCode = KeyCodes[ keys[ 0 ].toUpperCase() ]
if (this.isReadableKey(firstKeyCode)) {
// Some browsers (IE/FF) do not overwrite selected text, do it manually
// but only if the key is readable (some letter etc)
// do not clear the selection in case of special symbol
var selText = this.test.getSelectedText(el);
if (selText && 'value' in el && 'selectionStart' in el) {
var caretPos;
try {
caretPos = el.selectionStart;
} catch(e) {}
if (caretPos != null) {
// mimic replacing selected text
this.silentSetValue(el, el.value.substr(0, caretPos) + el.value.substr(caretPos + selText.length), 'value')
// Now set caret position to start of selection range
this.test.setCaretPosition(el, caretPos);
}
}
}
}
return new Promise(function (resolve, reject) {
queue.run(resolve)
})
},
simulateKeyPress: function (el, key, options) {
var isMac = bowser.mac;
var KeyCodes = Siesta.Test.UserAgent.KeyCodes().keys
var keyNameMap = Siesta.Test.UserAgent.KeyCodes().keyNameMap;
var keyCode = KeyCodes[ key.toUpperCase() ] || 0;
var keyDownEl = el = this.test.normalizeElement(el);
options = options || {};
options.readableKey = key;
// keypress should not be fired on Mac when CMD is pressed
// nor on Windows when CTRL is pressed
var suppressKeyPress = (isMac && options.metaKey) || (!isMac && options.ctrlKey);
// Should not actually type anything when CTRL / CMD are pressed
var isReadableKey = this.isReadableKey(keyCode);
var charCode = isReadableKey && !suppressKeyPress ? key.charCodeAt(0) : 0
options.key = isReadableKey ? key : (keyNameMap[ keyCode ] || '');
options.code = keyNameMap[ keyCode ];
var me = this,
isTextInput = me.isTextInput(el),
isEditableNode = me.isEditableNode(el),
acceptsTextInput = isTextInput || isEditableNode;
var textValueProp = 'value' in el ? 'value' : 'innerHTML';
var originalValue = isTextInput && el[ textValueProp ];
var originalLength = el[ textValueProp ].length;
var keyDownEvent = me.simulateEvent(el, 'keydown', Joose.O.extend({ charCode : 0, keyCode : keyCode }, options));
var keyDownPrevented = this.isEventPrevented(keyDownEvent)
var isSelection = acceptsTextInput && this.mimicTextSelection(keyDownEvent, el);
if (!isSelection) {
var keyPressPrevented = false;
var supports = Siesta.Project.Browser.FeatureSupport().supports
// Need to reevaluate focused element here, it may have changed in a 'keydown' listener
el = me.activeElement(true, el, el.ownerDocument);
// keypress should not be fired when CTRL or CMD are pressed
if (!suppressKeyPress && !keyDownPrevented) {
var event = me.simulateEvent(el, 'keypress', Joose.O.extend({ charCode : charCode, keyCode : isReadableKey ? 0 : keyCode }, options));
keyPressPrevented = this.isEventPrevented(event)
if (!keyPressPrevented && keyCode === KeyCodes.TAB) {
el = this.emulateTab(el, options.shiftKey ? -1 : 1) || el;
}
}
if (!keyDownPrevented && acceptsTextInput && keyCode != KeyCodes.TAB) {
if (isReadableKey && !suppressKeyPress && !keyPressPrevented) {
var innerHTML
// IE10 tries to be 'helpful' by inserting an empty space, clean it
// IE11 inserts <br> after call to the .focus() method of the element
if (isEditableNode && bowser.msie) {
innerHTML = el.innerHTML
if (innerHTML.indexOf('&nbsp;') === 0) {
el.innerHTML = innerHTML.substring(6)
originalLength = el.innerHTML.length
}
else if (innerHTML.indexOf('<br>') === 0) {
el.innerHTML = innerHTML.substring(4);
originalLength = el.innerHTML.length
}
}
// IE won't do execCommand with insertText
if (isEditableNode && !bowser.msie) {
innerHTML = el.innerHTML
if (innerHTML.charCodeAt(innerHTML.length - 1) === 8203) {
el.innerHTML = innerHTML.substring(0, innerHTML.length - 1);
}
el.ownerDocument.execCommand('insertText', false, options.readableKey);
}
else {
//TODO should check first if textInput event is supported
me.simulateEvent(el, bowser.msie ? 'textinput' : 'textInput', { text : options.readableKey });
}
// will fire 'input' event
me.mimicCharacterInsertion(el, key, options, originalLength);
}
else {
me.mimicCaretMovement(el, keyCode);
}
if (isTextInput && el[textValueProp] !== originalValue) {
el.valueWasModifiedByBackspace = false;
}
// Manually delete one char off the end if backspace simulation is not supported by the browser
if (
(keyCode === KeyCodes.BACKSPACE || keyCode === KeyCodes.DELETE)
&& !supports.canSimulateBackspace && el[ textValueProp ].length > 0
) {
this.mimicCharacterDeletion(el, keyCode, options);
if (isTextInput && el[ textValueProp ] !== originalValue) {
el.valueWasModifiedByBackspace = true;
}
}
if (textValueProp === 'value' && keyCode === KeyCodes.ENTER && !keyPressPrevented) {
if (isTextInput) this.maybeMimicChangeEvent(keyDownEl);
if (!supports.enterSubmitsForm) {
this.mimicFormSubmit(el);
}
}
}
}
this.mimicClickOnEnter(el, keyCode);
me.simulateEvent(el, 'keyup', $.extend({ charCode : 0, keyCode : keyCode }, options));
return Promise.resolve()
},
mimicCharacterInsertion : function (el, readableKey, options, originalLength) {
var textValueProp = 'value' in el ? 'value' : 'innerHTML';
var maxLength = el.getAttribute('maxlength') || Infinity
var isTextInput = this.isTextInput(el);
var supports = Siesta.Project.Browser.FeatureSupport().supports;
if (maxLength != null) maxLength = Number(maxLength)
// If the entered char had no impact on the textfield - manually put it there
if (
!el.readOnly && (isTextInput || bowser.msie)
&& !supports.canSimulateKeyCharacters
&& originalLength === el[ textValueProp ].length && originalLength < maxLength
) {
var val = el[ textValueProp ];
var caretPos = this.test.getCaretPosition(el);
// Fallback to appending text to end of string if caret position cannot be determined
if (caretPos == undefined) {
caretPos = val.length;
}
// Inject char at caret position
this.silentSetValue(
el,
val.substr(0, caretPos) + readableKey + val.substr(caretPos),
textValueProp
)
// Restore caret position
this.test.setCaretPosition(el, caretPos + 1);
this.simulateEvent(el, 'input', options);
}
},
// this method will change the property `propertyName` of the `el` to a `newValue`
// if `propertyName` will be "value" it will try to avoid "touching" the actual "value"
// property, since that may trigger side effects
// if user has defined own "value" property on the element (React did that, crazy)
silentSetValue : function (el, newValue, propertyName) {
var Object = this.global.Object
if (Object.getOwnPropertyDescriptor && propertyName == 'value') {
var desc = Object.getOwnPropertyDescriptor(el.constructor.prototype, propertyName)
desc.set.call(el, newValue)
} else
el[ propertyName ] = newValue;
},
mimicTextSelection : function(keyDownEvent, el) {
var isMac = bowser.mac;
var KC = Siesta.Test.UserAgent.KeyCodes().keys;
var retVal = false;
// CTRL-A or CMD-A in text input should select all
var ctrlKey = (!isMac && keyDownEvent.ctrlKey) || (keyDownEvent.metaKey && isMac);
switch (keyDownEvent.keyCode) {
// Select all
case KC["A"]:
if (ctrlKey) {
this.test.selectText(el);
retVal = true;
}
break;
case KC["LEFT"]:
case KC["HOME"]:
if (keyDownEvent.shiftKey) {
this.test.selectText(el, 0, this.test.getCaretPosition(el));
retVal = true;
}
break;
case KC["RIGHT"]:
case KC["END"]:
if (keyDownEvent.shiftKey) {
this.test.selectText(el, this.test.getCaretPosition(el));
retVal = true;
}
break;
}
return retVal;
},
mimicClickOnEnter : function (el, keyCode) {
// somehow "node.nodeName" is empty sometimes in IE10
var nodeName = el.nodeName && el.nodeName.toLowerCase()
var supports = Siesta.Project.Browser.FeatureSupport().supports
var KeyCodes = Siesta.Test.UserAgent.KeyCodes().keys
if ((nodeName == 'a' || nodeName == 'button') && keyCode === KeyCodes.ENTER && !supports.enterOnAnchorTriggersClick) {
// this "click" should not update the current cursor position its merely for activating "click" listeners
this.simulateEvent(el, 'click', { doNotUpdateCurrentPosition : true });
}
},
mimicCaretMovement : function(el, keyCode) {
// somehow "node.nodeName" is empty sometimes in IE10
var nodeName = el.nodeName && el.nodeName.toLowerCase()
if ((nodeName == 'input' || nodeName == 'textarea')) {
var KeyCodes = Siesta.Test.UserAgent.KeyCodes().keys;
switch (keyCode) {
case KeyCodes.HOME:
this.test.setCaretPosition(el, 0);
break;
case KeyCodes.LEFT:
var selText = this.test.getSelectedText(el);
if (selText) {
var caretPos = this.test.getCaretPosition(el);
this.test.selectText(el, caretPos, caretPos);
} else {
this.test.moveCaretPosition(el, -1);
}
break;
case KeyCodes.RIGHT:
var selText = this.test.getSelectedText(el);
if (selText) {
var caretPos = this.test.getCaretPosition(el);
this.test.selectText(el, caretPos + selText.length, caretPos + selText.length);
} else {
this.test.moveCaretPosition(el, 1);
}
break;
case KeyCodes.END:
this.test.setCaretPosition(el, el.value.length);
break;
}
}
},
mimicFormSubmit : function (el) {
var form = this.$(el).closest('form');
if (form.length) {
// Use jQuery's :submit instead of [type=submit] since <button>Foo</button> could have button.type=submit, but this is not queryable
var submitButton = form.find(':submit')[ 0 ];
var hasOneInput = form.find('input').length === 1;
if (submitButton) {
submitButton.click();
}
else if (hasOneInput) {
var submitPrevented = this.isEventPrevented(this.simulateEvent(form[ 0 ], 'submit', {}));
if (!submitPrevented) form[ 0 ].submit();
}
}
},
mimicCharacterDeletion : function (el, keyCode, options) {
var isTextInput = this.isTextInput(el);
if (!el.readOnly) {
// IE won't do execCommand with insertText
if (isTextInput || bowser.msie) {
var textValueProp = 'value' in el ? 'value' : 'innerHTML';
var text = el[ textValueProp ];
var selText = this.test.getSelectedText(el) || '';
var caretPosition = this.test.getCaretPosition(el);
var inputChanged = false
if (caretPosition != null && selText) {
this.silentSetValue(
el,
text.substring(0, caretPosition) + text.substring(caretPosition + selText.length),
textValueProp
)
inputChanged = true
} else {
var KeyCodes = Siesta.Test.UserAgent.KeyCodes().keys;
// fall back to last char index if caret position could not be determined
caretPosition = caretPosition == null ? text.length : caretPosition;
if (keyCode === KeyCodes.BACKSPACE) {
if (caretPosition > 0) {
inputChanged = true
this.silentSetValue(
el,
text.substring(0, caretPosition - 1) + text.substring(caretPosition),
textValueProp
)
caretPosition = caretPosition - 1;
}
} else {
if (caretPosition < text.length) inputChanged = true
// DELETE key
this.silentSetValue(
el,
(caretPosition > 0 ? text.substring(0, caretPosition + 1) : '') + text.substring(caretPosition + 1),
textValueProp
)
}
}
// Caret position is moved to end when setting text value, restore it manually
this.test.setCaretPosition(el, caretPosition);
inputChanged && this.simulateEvent(el, 'input', options);
} else {
el.ownerDocument.execCommand('delete');
}
}
},
maybeMimicChangeEvent : function (el) {
if (el.getAttribute('__lastValue') !== el.value) {
this.simulateEvent(el, 'change');
el.setAttribute('__lastValue', el.value);
}
}
}
});
</pre>
</body>
</html>