UNPKG

siesta-lite

Version:

Stress-free JavaScript unit testing and functional testing tool, works in NodeJS and browsers

720 lines (557 loc) 29.9 kB
<!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(&#39;Siesta.Test.Simulate.Keyboard&#39;, { requires : [ &#39;$&#39;, &#39;simulateEvent&#39;, &#39;isEventPrevented&#39; /*&#39;getSimulateEventsWith&#39;, &#39;getElementAtCursor&#39;*/ ], does : [ Siesta.Util.Role.CanFormatStrings, Siesta.Test.Browser.Role.CanWorkWithKeyboard ], has : { keyboardEventName : (&quot;KeyboardEvent&quot; in window &amp;&amp; !bowser.msedge) ? &quot;KeyboardEvent&quot; : (&quot;KeyEvent&quot; in window ? &quot;KeyEvents&quot; : 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 || &#39;&#39; }, options); // use W3C standard when available and allowed by &quot;simulateEventsWith&quot; option if (doc.createEvent &amp;&amp; this.getSimulateEventsWith() === &#39;dispatchEvent&#39;) { try { if (this.keyboardEventName === &#39;KeyboardEvent&#39;) { event = new this.global.KeyboardEvent(type, options); } } catch (err) { event = null; } if (!event) { event = doc.createEvent(&quot;Events&quot;); 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 &gt; 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(&#39;TextEvent&#39;); if (event &amp;&amp; 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 &lt;http://joelpurra.se/&gt; * 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.$(&quot;:focus, :input, a[href], [tabindex], body&quot;, this.getQueryableContainer(el)) .not(&quot;:disabled&quot;) .not(&quot;:hidden&quot;) .not(&quot;a[href]:empty&quot;) var escapeSelectorName = function (str) { // Based on http://api.jquery.com/category/selectors/ // Still untested return str.replace(/(!&quot;#$%&amp;&#39;\(\)\*\+,\.\/:;&lt;=&gt;\?@\[\]^`\{\|\}~)/g, &quot;\\\\$1&quot;); } var isRadio = false var selector if (el.tagName === &quot;INPUT&quot; &amp;&amp; el.type === &quot;radio&quot; &amp;&amp; el.name !== &quot;&quot; ) { isRadio = true selector = &quot;input[type=radio][name=&quot; + escapeSelectorName(el.name) + &quot;]&quot; } var processed = [] for (var i = 0; i &lt; $focusable.length; i++) { var currEl = $focusable[ i ] // always include current element if (currEl != el &amp;&amp; currEl.getAttribute(&#39;tabIndex&#39;) == -1 || isRadio &amp;&amp; $(currEl).is(selector)) continue processed.push(currEl) } var body = this.getBodyElement(el) var currentTabIndex = Number(el.getAttribute(&#39;tabIndex&#39;) || 0) var getTabIndex = function (dom) { if (dom === el &amp;&amp; currentTabIndex === -1) return 0 if (dom === body) return 0 return Number(dom.getAttribute(&#39;tabIndex&#39;) || 0) } processed.sort(function (a, b) { var aIndex = getTabIndex(a) var bIndex = getTabIndex(b) return aIndex &lt; bIndex ? -1 : (aIndex &gt; 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 &#39;Must supply a string to type&#39;; 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 (&#39;value&#39; in el) { el.setAttribute(&#39;__lastValue&#39;, el.value); } else if (el.isContentEditable) { // For contentEditable, walk up to find the root editable node var rootEditableEl = me.closest(el, &#39;[contentEditable]&#39;); if (rootEditableEl &amp;&amp; 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 + &#39;&#39;); 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 &quot;callbackDelay&quot; // so we don&#39;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 &quot;user agent&quot; code, // still allow to focus it, but using special &quot;param.focus&quot; 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 &quot;blur&quot; immediately after 1st focus // no Ext &quot;focus/blur&quot; methods seems to be called, so it can be a browser behavior bowser.msie &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; &#39;value&#39; in el &amp;&amp; &#39;selectionStart&#39; 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), &#39;value&#39;) // 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 &amp;&amp; options.metaKey) || (!isMac &amp;&amp; options.ctrlKey); // Should not actually type anything when CTRL / CMD are pressed var isReadableKey = this.isReadableKey(keyCode); var charCode = isReadableKey &amp;&amp; !suppressKeyPress ? key.charCodeAt(0) : 0 options.key = isReadableKey ? key : (keyNameMap[ keyCode ] || &#39;&#39;); options.code = keyNameMap[ keyCode ]; var me = this, isTextInput = me.isTextInput(el), isEditableNode = me.isEditableNode(el), acceptsTextInput = isTextInput || isEditableNode; var textValueProp = &#39;value&#39; in el ? &#39;value&#39; : &#39;innerHTML&#39;; var originalValue = isTextInput &amp;&amp; el[ textValueProp ]; var originalLength = el[ textValueProp ].length; var keyDownEvent = me.simulateEvent(el, &#39;keydown&#39;, Joose.O.extend({ charCode : 0, keyCode : keyCode }, options)); var keyDownPrevented = this.isEventPrevented(keyDownEvent) var isSelection = acceptsTextInput &amp;&amp; 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 &#39;keydown&#39; listener el = me.activeElement(true, el, el.ownerDocument); // keypress should not be fired when CTRL or CMD are pressed if (!suppressKeyPress &amp;&amp; !keyDownPrevented) { var event = me.simulateEvent(el, &#39;keypress&#39;, Joose.O.extend({ charCode : charCode, keyCode : isReadableKey ? 0 : keyCode }, options)); keyPressPrevented = this.isEventPrevented(event) if (!keyPressPrevented &amp;&amp; keyCode === KeyCodes.TAB) { el = this.emulateTab(el, options.shiftKey ? -1 : 1) || el; } } if (!keyDownPrevented &amp;&amp; acceptsTextInput &amp;&amp; keyCode != KeyCodes.TAB) { if (isReadableKey &amp;&amp; !suppressKeyPress &amp;&amp; !keyPressPrevented) { var innerHTML // IE10 tries to be &#39;helpful&#39; by inserting an empty space, clean it // IE11 inserts &lt;br&gt; after call to the .focus() method of the element if (isEditableNode &amp;&amp; bowser.msie) { innerHTML = el.innerHTML if (innerHTML.indexOf(&#39;&amp;nbsp;&#39;) === 0) { el.innerHTML = innerHTML.substring(6) originalLength = el.innerHTML.length } else if (innerHTML.indexOf(&#39;&lt;br&gt;&#39;) === 0) { el.innerHTML = innerHTML.substring(4); originalLength = el.innerHTML.length } } // IE won&#39;t do execCommand with insertText if (isEditableNode &amp;&amp; !bowser.msie) { innerHTML = el.innerHTML if (innerHTML.charCodeAt(innerHTML.length - 1) === 8203) { el.innerHTML = innerHTML.substring(0, innerHTML.length - 1); } el.ownerDocument.execCommand(&#39;insertText&#39;, false, options.readableKey); } else { //TODO should check first if textInput event is supported me.simulateEvent(el, bowser.msie ? &#39;textinput&#39; : &#39;textInput&#39;, { text : options.readableKey }); } // will fire &#39;input&#39; event me.mimicCharacterInsertion(el, key, options, originalLength); } else { me.mimicCaretMovement(el, keyCode); } if (isTextInput &amp;&amp; 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) &amp;&amp; !supports.canSimulateBackspace &amp;&amp; el[ textValueProp ].length &gt; 0 ) { this.mimicCharacterDeletion(el, keyCode, options); if (isTextInput &amp;&amp; el[ textValueProp ] !== originalValue) { el.valueWasModifiedByBackspace = true; } } if (textValueProp === &#39;value&#39; &amp;&amp; keyCode === KeyCodes.ENTER &amp;&amp; !keyPressPrevented) { if (isTextInput) this.maybeMimicChangeEvent(keyDownEl); if (!supports.enterSubmitsForm) { this.mimicFormSubmit(el); } } } } this.mimicClickOnEnter(el, keyCode); me.simulateEvent(el, &#39;keyup&#39;, $.extend({ charCode : 0, keyCode : keyCode }, options)); return Promise.resolve() }, mimicCharacterInsertion : function (el, readableKey, options, originalLength) { var textValueProp = &#39;value&#39; in el ? &#39;value&#39; : &#39;innerHTML&#39;; var maxLength = el.getAttribute(&#39;maxlength&#39;) || 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 &amp;&amp; (isTextInput || bowser.msie) &amp;&amp; !supports.canSimulateKeyCharacters &amp;&amp; originalLength === el[ textValueProp ].length &amp;&amp; originalLength &lt; 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, &#39;input&#39;, options); } }, // this method will change the property `propertyName` of the `el` to a `newValue` // if `propertyName` will be &quot;value&quot; it will try to avoid &quot;touching&quot; the actual &quot;value&quot; // property, since that may trigger side effects // if user has defined own &quot;value&quot; property on the element (React did that, crazy) silentSetValue : function (el, newValue, propertyName) { var Object = this.global.Object if (Object.getOwnPropertyDescriptor &amp;&amp; propertyName == &#39;value&#39;) { 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 &amp;&amp; keyDownEvent.ctrlKey) || (keyDownEvent.metaKey &amp;&amp; isMac); switch (keyDownEvent.keyCode) { // Select all case KC[&quot;A&quot;]: if (ctrlKey) { this.test.selectText(el); retVal = true; } break; case KC[&quot;LEFT&quot;]: case KC[&quot;HOME&quot;]: if (keyDownEvent.shiftKey) { this.test.selectText(el, 0, this.test.getCaretPosition(el)); retVal = true; } break; case KC[&quot;RIGHT&quot;]: case KC[&quot;END&quot;]: if (keyDownEvent.shiftKey) { this.test.selectText(el, this.test.getCaretPosition(el)); retVal = true; } break; } return retVal; }, mimicClickOnEnter : function (el, keyCode) { // somehow &quot;node.nodeName&quot; is empty sometimes in IE10 var nodeName = el.nodeName &amp;&amp; el.nodeName.toLowerCase() var supports = Siesta.Project.Browser.FeatureSupport().supports var KeyCodes = Siesta.Test.UserAgent.KeyCodes().keys if ((nodeName == &#39;a&#39; || nodeName == &#39;button&#39;) &amp;&amp; keyCode === KeyCodes.ENTER &amp;&amp; !supports.enterOnAnchorTriggersClick) { // this &quot;click&quot; should not update the current cursor position its merely for activating &quot;click&quot; listeners this.simulateEvent(el, &#39;click&#39;, { doNotUpdateCurrentPosition : true }); } }, mimicCaretMovement : function(el, keyCode) { // somehow &quot;node.nodeName&quot; is empty sometimes in IE10 var nodeName = el.nodeName &amp;&amp; el.nodeName.toLowerCase() if ((nodeName == &#39;input&#39; || nodeName == &#39;textarea&#39;)) { 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(&#39;form&#39;); if (form.length) { // Use jQuery&#39;s :submit instead of [type=submit] since &lt;button&gt;Foo&lt;/button&gt; could have button.type=submit, but this is not queryable var submitButton = form.find(&#39;:submit&#39;)[ 0 ]; var hasOneInput = form.find(&#39;input&#39;).length === 1; if (submitButton) { submitButton.click(); } else if (hasOneInput) { var submitPrevented = this.isEventPrevented(this.simulateEvent(form[ 0 ], &#39;submit&#39;, {})); if (!submitPrevented) form[ 0 ].submit(); } } }, mimicCharacterDeletion : function (el, keyCode, options) { var isTextInput = this.isTextInput(el); if (!el.readOnly) { // IE won&#39;t do execCommand with insertText if (isTextInput || bowser.msie) { var textValueProp = &#39;value&#39; in el ? &#39;value&#39; : &#39;innerHTML&#39;; var text = el[ textValueProp ]; var selText = this.test.getSelectedText(el) || &#39;&#39;; var caretPosition = this.test.getCaretPosition(el); var inputChanged = false if (caretPosition != null &amp;&amp; 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 &gt; 0) { inputChanged = true this.silentSetValue( el, text.substring(0, caretPosition - 1) + text.substring(caretPosition), textValueProp ) caretPosition = caretPosition - 1; } } else { if (caretPosition &lt; text.length) inputChanged = true // DELETE key this.silentSetValue( el, (caretPosition &gt; 0 ? text.substring(0, caretPosition + 1) : &#39;&#39;) + text.substring(caretPosition + 1), textValueProp ) } } // Caret position is moved to end when setting text value, restore it manually this.test.setCaretPosition(el, caretPosition); inputChanged &amp;&amp; this.simulateEvent(el, &#39;input&#39;, options); } else { el.ownerDocument.execCommand(&#39;delete&#39;); } } }, maybeMimicChangeEvent : function (el) { if (el.getAttribute(&#39;__lastValue&#39;) !== el.value) { this.simulateEvent(el, &#39;change&#39;); el.setAttribute(&#39;__lastValue&#39;, el.value); } } } }); </pre> </body> </html>