UNPKG

siesta-lite

Version:

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

1,141 lines (948 loc) 83.6 kB
/* Siesta 5.6.1 Copyright(c) 2009-2022 Bryntum AB https://bryntum.com/contact https://bryntum.com/products/siesta/license */ /** @class Siesta.Test.Element This is a mixin, with helper methods for testing functionality relating to DOM elements. This mixin is consumed by {@link Siesta.Test} */ Role('Siesta.Test.Element', { does : [ Siesta.Util.Role.CanCalculatePageScroll, Siesta.Util.Role.Dom ], requires : [ 'typeOf', 'chain', 'normalizeElement' ], has : { allowMonkeyToClickOnAnchors : false, allowedCharacters : function () { return { // does not include TAB by purpose, because our "TAB" simulation is not perfect // Also exclude BACKSPACE since it navigates the page special : 'ENTER/ESCAPE/PAGE-UP/PAGE-DOWN/END/HOME/UP/RIGHT/DOWN/LEFT/INSERT/DELETE', // does not inlcude * because Ext fails on typing it punctuation : '.,/()[]{}\\"\'`~!?@#$%^&_=+-', normal : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" } } }, methods : { /** * Utility method which returns the center of a passed element. The coordinates are by default relative to the * containing document of the element (so for example if the element is inside of the nested iframe, coordinates * will be "local" to that iframe element). To get coordinates relative to the test iframe ("global" coordinates), * pass `local` as `false`. * * @param {Siesta.Test.ActionTarget} el The element to find the center of. * @param {Boolean} [local] Pass `true` means coordinates are relative to the containing document. This is the default value. * Pass `false` to make sure the coordinates are global to the test window. * * @return {Array} The array first element of which is the `x` coordinate and 2nd - `y` */ findCenter : function (target, local) { return this.getTargetCoordinate(target, local); }, normalizeOffset : function (offset, $el) { var parts; if (this.typeOf(offset) === 'Function') offset = offset.call(this) offset = offset && offset.slice() || [ '50%', '50%' ]; var rect = this.getBoundingClientRect($el[ 0 ]) // The rounding rules are still magical // one is for sure, that if we get a precise whole number w/o fractional part // we subtract 1 from it, since pixel counting starts from 0 // when we have fractional part, in good browsers it seems its enough to throw it away with `floor` // in IE we additionally need to subtract 1 // following the conservative path and subtracting 1 in all browsers var width = Math.floor(rect.width) // if (bowser.msie || bowser.edge || width === rect.width) width-- width-- var height = Math.floor(rect.height) // if (bowser.msie || bowser.edge || height === rect.height) height-- height-- if (typeof (offset[ 0 ]) === 'string') { parts = offset[ 0 ].split('%'); offset[ 0 ] = parseInt(offset[ 0 ].match(/\d+/)[ 0 ], 10) * width / 100; if (parts[ 1 ]) { offset[ 0 ] += parseInt(parts[ 1 ]); } } offset[ 0 ] = Math.round(offset[ 0 ]) if (typeof (offset[ 1 ]) === 'string') { parts = offset[ 1 ].split('%'); offset[ 1 ] = parseInt(offset[ 1 ].match(/\d+/)[ 0 ], 10) * height / 100; if (parts[ 1 ]) { offset[ 1 ] += parseInt(parts[ 1 ]); } } offset[ 1 ] = Math.round(offset[ 1 ]) return offset }, // return viewport coordinates // can be simplified with just `getBoundingClientRect` ? getTargetCoordinate : function (target, local, offset) { var el = this.normalizeElement(target), $el = this.$(el), elOffset = $el.offset(), elDoc = el.ownerDocument, elWin = elDoc.defaultView || elDoc.parentWindow, xy = [ this.pageXtoViewportX(elOffset.left, elWin), this.pageYtoViewportY(elOffset.top, elWin) ] offset = this.normalizeOffset(offset, $el) xy[ 0 ] += offset[ 0 ]; xy[ 1 ] += offset[ 1 ]; if (local === false) { // Potentially we're interacting with an element inside a nested frame, which means // the coordinates are local to that frame if (elWin && elWin !== this.global) { var offsetsToTop = this.$(elWin.frameElement).offset() xy[ 0 ] += this.pageXtoViewportX(offsetsToTop.left) xy[ 1 ] += this.pageYtoViewportY(offsetsToTop.top) } } return xy; }, /** * Returns true if the element is visible, checking jQuery :visible selector + style visibility value. * * @param {Siesta.Test.ActionTarget} el The element * @return {Boolean} */ isElementVisible : function(el) { el = this.normalizeElement(el); // Workaround for OPTION elements which don't behave like normal DOM elements. jQuery always consider them invisible. // Decide based on visibility of the parent SELECT node if (el && el.nodeName.toLowerCase() === 'option') { el = this.$(el).closest('select')[0] } if (el) { try { // Jquery :visible doesn't handle SVG/VML, so manual check // accessing to `this.global.SVGElement` throws exceptions for popups in IE 9 if (window.SVGElement && el instanceof this.global.SVGElement) return el.style.display !== 'none' && el.style.visibility !== 'hidden' } catch (e) { } // Jquery :visible doesn't take visibility into account return this.$(el).is(':visible') && (this.$(el).css('visibility') !== 'hidden') } return false }, /** * Passes if the `innerText` property of the &lt;body&gt; element contains the text passed * * @param {String} text The text to match * @param {String} [description] The description for the assertion */ assertTextPresent : function(text, description) { this.like(this.global.document.body.innerText, text, description); }, /** * Passes if the innerHTML of the passed element contains the text passed * * @param {Siesta.Test.ActionTarget} el The element to query * @param {String} text The text to match * @param {String} [description] The description for the assertion */ contentLike : function(el, text, description) { el = this.normalizeElement(el); this.like(el.innerHTML, text, description); }, /** * Passes if the innerHTML of the passed element does not contain the text passed * * @param {Siesta.Test.ActionTarget} el The element to query * @param {String} text The text to match * @param {String} [description] The description for the assertion */ contentNotLike : function(el, text, description) { el = this.normalizeElement(el); this.unlike(el.innerHTML, text, description); }, /** * Waits until the innerHTML of the passed element contains the text passed * * @param {Siesta.Test.ActionTarget} el The element to query * @param {String} text The text to match * @param {Function} callback The callback to call after the CSS selector has been found * @param {Object} scope The scope for the callback * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value. */ waitForContentLike : function(el, text, callback, scope, timeout) { var R = Siesta.Resource('Siesta.Test.Element'); el = this.normalizeElement(el); return this.waitFor({ method : function() { return el.innerHTML.match(text); }, callback : callback, scope : scope, timeout : timeout, assertionName : 'waitForContentLike', description : ' ' + R.get('elementContent') + ' "' + text + '" ' + R.get('toAppear') }); }, /** * Waits until the innerHTML of the passed element does not contain the text passed * * @param {Siesta.Test.ActionTarget} el The element to query * @param {String} text The text to match * @param {Function} callback The callback to call after the CSS selector has been found * @param {Object} scope The scope for the callback * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value. */ waitForContentNotLike : function(el, text, callback, scope, timeout) { var R = Siesta.Resource('Siesta.Test.Element'); el = this.normalizeElement(el); return this.waitFor({ method : function() { return !el.innerHTML.match(text); }, callback : callback, scope : scope, timeout : timeout, assertionName : 'waitForContentNotLike', description : ' ' + R.get('elementContent') + ' "' + text + '" ' + R.get('toDisappear') }); }, getRandomTypeString : function (length) { var allowedCharacters = this.allowedCharacters var special = allowedCharacters.special.split('/') var punctuation = allowedCharacters.punctuation var normal = allowedCharacters.normal var total = special.length + punctuation.length + normal.length var str = '' for (var i = 0; i < length; i++) { var index = this.randomBetween(0, total - 1) if (index < normal.length) str += normal.substr(index, 1) else { index -= normal.length if (index < punctuation.length) str += punctuation.substr(index, 1) else { index -= punctuation.length str += '[' + special[ index ] + ']' } } } return str }, /** * Performs clicks, double clicks, right clicks and drags at random coordinates within the passed target. * Also performs random key presses. You can specify an array of DOM selectors to avoid (see `skipTargets` below). * * While doing all these random actions it tracks the number of exceptions thrown and reports a failure * if there was any. Otherwise it reports a passed assertion. * * Use this assertion to "stress-test" your component, making sure it will work correctly in various unexpected * interaction scenarious. * * Note that as a special case, when this method is provided with the document's &lt;body&gt; element as a target, * it will test the whole browser viewport. * * This method supports two call signatures. One is with multiple positional arguments and another with single options object. * * @param {Siesta.Test.ActionTarget | Object} options The element to upon which to unleash the "monkey", or a config object with the options * @param {Siesta.Test.ActionTarget} options.target The element to upon which to unleash the "monkey" * @param {Array[String]} options.skipTargets An array of DOM selectors, for the elements inside the `target`, which will be avoided by monkeys. * @param {Number} options.nbrInteractions The number of random interactions to perform. Default value is 30 * @param {String} options.description The optional description for the assertion. * @param {Function} options.callback The callback to call after all actions are completed * @param {Object} options.scope The scope for the callback * @param {Number} nbrInteractions The number of random interactions to perform. Default value is 30 * @param {String} [description] The description for the assertion * @param {Function} callback The callback to call after all actions are completed * @param {Object} scope The scope for the callback */ monkeyTest : function (el, nbrInteractions, description, callback, scope, stepCallback) { var skipTargets var alwaysLogActions = false if (arguments.length === 1 && el.target) { nbrInteractions = el.nbrInteractions description = el.description callback = el.callback scope = el.scope stepCallback = el.stepCallback skipTargets = el.skipTargets alwaysLogActions = Boolean(el.alwaysLogActions) el = el.target } else { if (typeof nbrInteractions === 'function') { callback = nbrInteractions; scope = description; description = ''; } else if (typeof description === 'function') { callback = description; description = ''; } } skipTargets = skipTargets || [] // ignore actions on anchors by default, to prevent page navigation skipTargets.push('a') nbrInteractions = typeof nbrInteractions === 'number' ? nbrInteractions : 30; el = this.normalizeElement(el, false, true); this.suppressPassedWaitForAssertion = true; var global = this.global var isBody = el == global.document.body var me = this, root = this.getRootTest(), offset = me.$(el).offset(), right = offset.left + me.$(isBody ? global : el).width(), bottom = offset.top + me.$(isBody ? global : el).height(); var actionLog = [] var R = Siesta.Resource('Siesta.Test.Element'); var queue = new Siesta.Util.Queue({ deferer : me.originalSetTimeout, deferClearer : me.originalClearTimeout, interval : me.actionDelay, observeTest : this, processor : function (data) { if (root.nbrExceptions || root.failed) { assertionChecker() // do not continue if the test has detected an exception thrown queue.abort() } else { var async = me.beginAsync(null, function (test) { test.fail( description || R.get('monkeyNoExceptions'), R.get('monkeyActionLog') + ":" + JSON.stringify(actionLog) ) return true }); var next = data.next data.next = function () { // callback to call after each monkey action stepCallback && stepCallback(data) me.endAsync(async) next() } data.action(data) } } }); var dummy = [] dummy.length = nbrInteractions var ignoreAction = function (data, i) { var target = me.normalizeElement(data.dragFrom || data.xy) // Prevent also drag target as it could be a link tag var dragToTarget = data.dragTo && me.normalizeElement(data.dragTo) for (var i = 0; i < skipTargets.length; i++) { var toSkip = me.query(skipTargets[ i ], el) for (var j = 0; j < toSkip.length; j++) { var skip = toSkip[ j ] if ( (target && (target == skip || me.contains(skip, target))) || dragToTarget && (dragToTarget == skip || me.contains(skip, dragToTarget)) ) { data.next() return true } } } return false } Joose.A.each(dummy, function (value, i) { // Inject { waitForSelector : 'body' } before every monkey action to make sure we always have a body queue.addAsyncStep({ action : function (data) { me.waitForSelector('body', data.next); } }); var xy = [ me.randomBetween(offset.left, right), me.randomBetween(offset.top, bottom) ]; switch (me.randomBetween(0, 4)) { case 0: queue.addAsyncStep({ action : function (data) { if (!ignoreAction(data)) { actionLog.push({ 'click' : xy }) me.click(data.xy, data.next) } }, xy : xy }); break; case 1: queue.addAsyncStep({ action : function (data) { if (!ignoreAction(data)) { actionLog.push({ 'doubleclick' : xy }) me.doubleClick(data.xy, data.next) } }, xy : xy }); break; case 2: queue.addAsyncStep({ action : function (data) { if (!ignoreAction(data)) { if (me.simulator.type != 'native' && "oncontextmenu" in window) { actionLog.push({ 'rightclick' : xy }) me.rightClick(data.xy, data.next) } else { actionLog.push({ 'click' : xy }) me.click(data.xy, data.next) } } }, xy : xy }); break; case 3: var dragTo = [ me.randomBetween(offset.left, right), me.randomBetween(offset.top, bottom) ] queue.addAsyncStep({ action : function (data) { if (!ignoreAction(data)) { actionLog.push({ action : 'drag', target : xy, to : dragTo }) me.dragTo(data.dragFrom, data.dragTo, data.next) } }, dragFrom : xy, dragTo : dragTo }); break; case 4: var text = me.getRandomTypeString(15) // First click somewhere then type queue.addAsyncStep({ action : function (data) { if (!ignoreAction(data)) { actionLog.push({ 'click' : xy }) me.click(data.xy, function () { me.waitForSelector('body', function () { actionLog.push({ 'type' : text.replace("'", "\\'") }) me.type(null, text, data.next) }); }) } }, xy : xy }); break; } }) var checkerActivated = false var assertionChecker = function () { checkerActivated = true if (root.nbrExceptions || root.failed) { me.fail(description || R.get('monkeyNoExceptions'), R.get('monkeyActionLog') + ":" + JSON.stringify(actionLog)) } else me.pass(description || R.get('monkeyNoExceptions'), alwaysLogActions ? R.get('monkeyActionLog') + ":" + JSON.stringify(actionLog) : undefined) } this.on('beforetestfinalizeearly', assertionChecker) queue.run(function () { if (!checkerActivated) { me.un('beforetestfinalizeearly', assertionChecker) assertionChecker() } me.suppressPassedWaitForAssertion = false; me.processCallbackFromTest(callback, [actionLog], scope || me) }); }, /** * Passes if the element has the supplied CSS classname * * @param {Siesta.Test.ActionTarget} el The element to query * @param {String} cls The class name to check for * @param {String} [description] The description for the assertion */ hasCls : function (el, cls, description) { var R = Siesta.Resource('Siesta.Test.Element'); el = this.normalizeElement(el); if (el.classList.contains(cls)) { this.pass(description, { descTpl : R.get('elementHasClass') + ' {cls}', cls : cls }); } else { this.fail(description, { assertionName : 'hasCls', got : el.className, gotDesc : R.get('elementClasses'), need : cls, needDesc : R.get('needClass') }) } }, /** * Passes if the element does not have the supplied CSS classname * * @param {Siesta.Test.ActionTarget} el The element to query * @param {String} cls The class name to check for * @param {String} [description] The description for the assertion */ hasNotCls : function (el, cls, description) { var R = Siesta.Resource('Siesta.Test.Element'); el = this.normalizeElement(el); if (!el.classList.contains(cls)) { this.pass(description, { descTpl : R.get('elementHasNoClass') + ' {cls}', cls : cls }); } else { this.fail(description, { assertionName : 'hasNotCls', got : el.className, gotDesc : R.get('elementClasses'), annotation : R.get('elementHasClass') + ' [' + cls + ']' }) } }, /** * Passes if the element has the supplied style value * * @param {Siesta.Test.ActionTarget} el The element to query * @param {String} property The style property to check for * @param {String} value The style value to check for * @param {String} [description] The description for the assertion */ hasStyle : function (el, property, value, description) { var R = Siesta.Resource('Siesta.Test.Element'); el = this.normalizeElement(el); if (this.$(el).css(property) === value) { this.pass(description, { descTpl : R.get('hasStyleDescTpl'), value : value, property : property }); } else { this.fail(description, { assertionName : 'hasStyle', got : this.$(el).css(property), gotDesc : R.get('elementStyles'), need : value, needDesc : R.get('needStyle') }); } }, /** * Passes if the element does not have the supplied style value * * @param {Siesta.Test.ActionTarget} el The element to query * @param {String} property The style property to check for * @param {String} value The style value to check for * @param {String} [description] The description for the assertion */ hasNotStyle : function (el, property, value, description) { var R = Siesta.Resource('Siesta.Test.Element'); el = this.normalizeElement(el); if (this.$(el).css(property) !== value) { this.pass(description, { descTpl : R.get('hasNotStyleDescTpl'), value : value, property : property }); } else { this.fail(description, { assertionName : 'hasNotStyle', got : el.style.toString(), gotDesc : R.get('elementStyles'), annotation : R.get('hasTheStyle') + ' [' + property + ']' }); } }, /** * Waits for a certain CSS selector to be found at the passed XY coordinate, and calls the callback when found. * The callback will receive the element from the passed XY coordinates. * * @param {Array} xy The x and y coordinates to query * @param {String} selector The CSS selector to check for * @param {Function} callback The callback to call after the CSS selector has been found * @param {Object} scope The scope for the callback * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test#waitForTimeout} value. */ waitForSelectorAt : function(xy, selector, callback, scope, timeout) { var R = Siesta.Resource('Siesta.Test.Element'); if (!selector) throw R.get('noCssSelector'); var me = this return this.waitFor({ method : function() { var el = me.elementFromPoint(xy[0], xy[1], true); if (el && me.$(el).is(selector)) return el; }, callback : callback, scope : scope, timeout : timeout, assertionName : 'waitForSelectorAt', description : ' ' + R.get('selector') + ' "' + selector + '" ' + R.get('toAppearAt') + ': [' + xy.toString() + ']' }); }, /** * Waits for a certain CSS selector to be found at current cursor position, and calls the callback when found. * The callback will receive the element found. * * @param {String} selector The CSS selector to check for * @param {Function} callback The callback to call after the CSS selector has been found * @param {Object} scope The scope for the callback * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test#waitForTimeout} value. */ waitForSelectorAtCursor : function(selector, callback, scope, timeout) { return this.waitForSelectorAt(this.simulator.currentPosition, selector, callback, scope, timeout); }, /** * Waits for a certain CSS selector to be found in the DOM, and then calls the callback supplied. * The callback will receive the results of jQuery selector. * * @param {String} selector The CSS selector to check for * @param {Siesta.Test.ActionTarget} root (optional) The root element in which to detect the selector. * @param {Function} callback The callback to call after the CSS selector has been found * @param {Object} scope The scope for the callback * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test#waitForTimeout} value. */ waitForSelector : function(selector, root, callback, scope, timeout) { var R = Siesta.Resource('Siesta.Test.Element'); var me = this; if (!selector) throw R.get('noCssSelector'); if (this.typeOf(root) == 'Function') { timeout = scope; scope = callback; callback = root; root = null; } if (root) root = this.normalizeElement(root); return this.waitFor({ method : function() { var result = me.query(selector, root); if (result.length > 0) return result; }, callback : callback, scope : scope, timeout : timeout, assertionName : 'waitForSelector', description : ' ' + R.get('selector') + ' "' + selector + '" ' + R.get('toAppear') }); }, /** * Waits till all the CSS selectors from the provided array to be found in the DOM, and then calls the callback supplied. * * @param {Array[String]} selectors The array of CSS selectors to check for * @param {Siesta.Test.ActionTarget} root (optional) The root element in which to detect the selector. * @param {Function} callback The callback to call after the CSS selector has been found * @param {Object} scope The scope for the callback * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test#waitForTimeout} value. */ waitForSelectors : function(selectors, root, callback, scope, timeout) { var R = Siesta.Resource('Siesta.Test.Element'); if (selectors.length < 1) throw R.get('waitForSelectorsBadInput'); if (this.typeOf(root) == 'Function') { timeout = scope; scope = callback; callback = root; root = null; } if (root) root = this.normalizeElement(root); var me = this return this.waitFor({ method : function () { var allPresent = true Joose.A.each(selectors, function (selector) { if (me.query(selector, root).length === 0) { allPresent = false // stop iteration return false } }) return allPresent }, callback : callback, scope : scope, timeout : timeout, assertionName : 'waitForSelectors', description : ' ' + R.get('selectors') + ' "' + selectors + '" ' + R.get('toAppear') }); }, /** * Waits for a certain CSS selector to not be found in the DOM, and then calls the callback supplied. * * @param {String} selector The CSS selector to check for * @param {Siesta.Test.ActionTarget} root (optional) The root element in which to detect the selector. * @param {Function} callback The callback to call after the CSS selector has been found * @param {Object} scope The scope for the callback * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test#waitForTimeout} value. */ waitForSelectorNotFound : function(selector, root, callback, scope, timeout) { var R = Siesta.Resource('Siesta.Test.Element'); var me = this; if (!selector) throw 'A CSS selector must be supplied'; if (this.typeOf(root) == 'Function') { timeout = scope; scope = callback; callback = root; root = null; } if (root) root = this.normalizeElement(root); return this.waitFor({ method : function() { return me.query(selector, root).length === 0; }, callback : callback, scope : scope, timeout : timeout, assertionName : 'waitForSelectorNotFound', description : ' ' + R.get('selector') + ' "' + selector + '" ' + R.get('toDisappear') }); }, /** * Waits until the passed element becomes "visible" in the DOM and calls the provided callback. * Please note, that "visible" means element will just have a DOM node, and still may be hidden by another visible element. * * The callback will receive the passed element as the 1st argument. * * See also {@link #waitForElementTop} method. * * @param {Siesta.Test.ActionTarget} el The element to look for. * @param {Function} callback The callback to call after the CSS selector has been found * @param {Object} scope The scope for the callback * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value. */ waitForElementVisible : function(el, callback, scope, timeout) { var R = Siesta.Resource('Siesta.Test.Element'); return this.waitFor({ method : function() { var normalized = this.normalizeElement(el, true); if (normalized && this.isElementVisible(normalized)) return normalized; }, callback : callback, scope : scope, timeout : timeout, assertionName : 'waitForElementVisible', description : ' ' + R.get('element') + ' "' + el.toString() + '" ' + R.get('toAppear') }); }, /** * Waits until the passed element is becomes not "visible" in the DOM and call the provided callback. * Please note, that "visible" means element will just have a DOM node, and still may be hidden by another visible element. * * The callback will receive the passed element as the 1st argument. * * See also {@link #waitForElementNotTop} method. * * @param {Siesta.Test.ActionTarget} el The element to look for. * @param {Function} callback The callback to call after the CSS selector has been found * @param {Object} scope The scope for the callback * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value. */ waitForElementNotVisible : function(el, callback, scope, timeout) { el = this.normalizeElement(el); var R = Siesta.Resource('Siesta.Test.Element'); var me = this; return this.waitFor({ method : function() { return !me.isElementVisible(el) && el; }, callback : callback, scope : scope, timeout : timeout, assertionName : 'waitForElementNotVisible', description : ' ' + R.get('element') + ' "' + el.toString() + '" ' + R.get('toDisappear') }); }, /** * Waits until the passed element is the 'top' element in the DOM and call the provided callback. * * The callback will receive the passed element as the 1st argument. * * @param {Siesta.Test.ActionTarget} el The element to look for. * @param {Function} callback The callback to call * @param {Object} scope The scope for the callback * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value. */ waitForElementTop : function(el, callback, scope, timeout) { var R = Siesta.Resource('Siesta.Test.Element'); return this.waitFor({ method : function() { var normalized = this.normalizeElement(el, true); if (normalized && this.elementIsTop(normalized, true)) { return normalized; } }, callback : callback, scope : scope, timeout : timeout, assertionName : 'waitForElementTop', description : ' ' + R.get('element') + ' "' + el.toString() + '" ' + R.get('toBeTopEl') }); }, /** * Waits until the passed element is not the 'top' element in the DOM and calls the provided callback with the element found. * * The callback will receive the actual top element. * * @param {Siesta.Test.ActionTarget} el The element to look for. * @param {Function} callback The callback to call * @param {Object} scope The scope for the callback * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value. */ waitForElementNotTop : function(el, callback, scope, timeout) { el = this.normalizeElement(el); var R = Siesta.Resource('Siesta.Test.Element'); var me = this return this.waitFor({ method : function() { if (!me.elementIsTop(el, true)) { var center = me.findCenter(el); return me.elementFromPoint(center[0], center[1], true); } }, callback : callback, scope : scope, timeout : timeout, assertionName : 'waitForElementNotTop', description : ' ' + R.get('element') + ' "' + el.toString() + '" ' + R.get('toNotBeTopEl') }); }, /** * Passes if the element is in the DOM and visible. * @param {Siesta.Test.ActionTarget} el The element * @param {String} [description] The description for the assertion */ elementIsVisible : function(el, description) { el = this.normalizeElement(el, true, false, false, { ignoreNonVisible : false }); this.ok(el && this.isElementVisible(el), description); }, /** * Passes if the element is not visible. * @param {Siesta.Test.ActionTarget} el The element * @param {String} [description] The description for the assertion */ elementIsNotVisible : function(el, description) { el = this.normalizeElement(el, false, false, false, { ignoreNonVisible : false }); this.notOk(this.isElementVisible(el), description); }, /** * Utility method which checks if the passed method is the 'top' element at its position. By default, "top" element means, * that center point of the element is not covered with any other elements. You can also check any other point reachability * using the "offset" argument. * * @param {Siesta.Test.ActionTarget} el The element to look for. * @param {Boolean} allowChildren true to also include child nodes. False to strictly check for the passed element. * @param {Array} offset An array of 2 elements, defining "x" and "y" offset from the left-top corner of the element * * @return {Boolean} true if the element is the top element. */ elementIsTop : function (el, allowChildren, offset) { el = this.normalizeElement(el); if (this.nodeIsOrphan(el)) { return false; } // Workaround for OPTION elements which don't behave like normal DOM elements. jQuery always consider them invisible. // Decide based on visibility of the parent SELECT node if (el && el.nodeName.toLowerCase() === 'option') { el = this.$(el).closest('select')[0]; } var fromPointAPI = this.getQueryableContainer(el) var localPoint = this.getTargetCoordinate(el, true, offset) var foundEl = fromPointAPI.elementFromPoint(localPoint[ 0 ], localPoint[ 1 ]); return foundEl && (foundEl === el || (allowChildren && this.$(foundEl).closest(el).length > 0)); }, // Helper method to find out if an offset is targeting a point outside its target // Assumes the el passed is visible isOffsetInsideElementBox : function (el, offset) { if (!offset) return true; var $el = this.$(this.normalizeElement(el)); var w = $el.outerWidth(); var h = $el.outerHeight(); offset = this.normalizeOffset(offset, $el); return offset[0] >= 0 && offset[0] < w && offset[1] >= 0 && offset[1] < h; }, /** * Passes if the element is found at the supplied xy coordinates. * * @param {Siesta.Test.ActionTarget} el The element to query * @param {Array} xy The xy coordinate to query. * @param {Boolean} allowChildren true to also include child nodes. False to strictly check for the passed element. * @param {String} [description] The description for the assertion */ elementIsAt : function(el, xy, allowChildren, description) { el = this.normalizeElement(el); var foundEl = this.elementFromPoint(xy[0], xy[1], true); var R = Siesta.Resource('Siesta.Test.Element'); if (!foundEl) { this.fail(description, { assertionName : 'elementIsAt', got : { x: xy[0], y : xy[1] }, gotDesc : R.get('Position'), annotation : R.get('noElementAtPosition') }); } else if (allowChildren) { if (foundEl === el || this.$(foundEl).closest(el).length > 0) { this.pass(description, { descTpl : R.get('elementIsAtDescTpl'), x : xy[ 0 ], y : xy[ 1 ] }); } else { this.fail(description, { assertionName : 'elementIsAt', got : foundEl, gotDesc : R.get('topElement'), need : el, needDesc : R.get('allowChildrenDesc'), annotation : R.get('allowChildrenAnnotation') }); } } else { if (foundEl === el) { this.pass(description, { descTpl : R.get('elementIsAtPassTpl'), x : xy[ 0 ], y : xy[ 1 ] }); } else { this.fail(description, { assertionName : 'elementIsAt', got : foundEl, gotDesc : R.get('topElement'), need : el, needDesc : 'Should be', annotation : R.get('noChildrenFailAnnotation') }); } } }, /** * Passes if the element is the top element (using its center xy coordinates). "Top" element means, * that element is not covered with any other elements. * * This assertion can be used for example to test, that some element, that appears only when mouse hovers some other element is accessible by user * with mouse (which is not always true because of various z-index issues). * * @param {Siesta.Test.ActionTarget} el The element to look for. * @param {Boolean} allowChildren true to also include child nodes. False to strictly check for the passed element. * @param {String} [description] The description for the assertion * @param {Boolean} strict true to check all four corners of the element. False to only check at element center. */ elementIsTopElement : function(el, allowChildren, description, strict) { el = this.normalizeElement(el); if (strict) { var o = this.$(el).offset(); var R = Siesta.Resource('Siesta.Test.Element'); var region = { top : o.top, right : o.left + this.$(el).outerWidth(), bottom : o.top + this.$(el).outerHeight(), left : o.left }; this.elementIsAt(el, [region.left+1, region.top+1], allowChildren, description + ' ' + R.get('topLeft')); this.elementIsAt(el, [region.left+1, region.bottom-1], allowChildren, description + ' ' + R.get('bottomLeft')); this.elementIsAt(el, [region.right-1, region.top+1], allowChildren, description + ' ' + R.get('topRight')); this.elementIsAt(el, [region.right-1, region.bottom-1], allowChildren, description + ' ' + R.get('bottomRight')); } else { this.elementIsAt(el, this.findCenter(el), allowChildren, description); } }, /** * Passes if the element is not the top element (using its center xy coordinates). * * @param {Siesta.Test.ActionTarget} el The element to look for. * @param {Boolean} allowChildren true to also include child nodes. False to strictly check for the passed element. * @param {String} [description] The description for the assertion */ elementIsNotTopElement : function(el, allowChildren, description) { el = this.normalizeElement(el); var center = this.findCenter(el); var foundEl = this.elementFromPoint(center[ 0 ], center[ 1 ], true); if (!foundEl) { var R = Siesta.Resource('Siesta.Test.Element'); this.pass(description, {