/* Siesta 5.6.1 Copyright(c) 2009-2022 Bryntum AB https://bryntum.com/contact https://bryntum.com/products/siesta/license */ /** @class Siesta.Test.Browser @extends Siesta.Test @mixin Siesta.Test.TextSelection @mixin Siesta.Test.Element @mixin Siesta.Test.UserAgent.Mouse @mixin Siesta.Test.UserAgent.Keyboard @mixin Siesta.Test.UserAgent.Touch @mixin Siesta.Test.Browser.Role.CanGetElementFromPoint A base class for testing a generic browser functionality. It has various DOM-related assertions, and is not optimized for any framework. */ Class('Siesta.Test.Browser', { isa : Siesta.Test, does : [ Siesta.Util.Role.CanParseBrowser, Siesta.Util.Role.CanCalculatePageScroll, Siesta.Util.Role.Dom, Siesta.Test.Browser.Role.CanGetElementFromPoint, Siesta.Test.Browser.Role.CanWorkWithKeyboard, Siesta.Test.Browser.Role.CanRebindJQueryContext, Siesta.Test.UserAgent.Mouse, Siesta.Test.UserAgent.Keyboard, Siesta.Test.UserAgent.Touch, Siesta.Test.Element, Siesta.Test.TextSelection, Siesta.Test.Observable ], has : { /** * @property {Object} bowser An instance of browser detection library - [Bowser](https://github.com/ded/bowser). * Please refer to the provided link for the detailed documentation, here we just provide some examples how * it can be used in the test file: * // Browser detection if (t.bowser.chrome && t.bowser.version > 50) { .. do something .. } if (t.bowser.msie && t.bowser.version >= 10) { .. do something .. } // OS detection if (t.bowser.mac) { .. do something .. } // Rendering engine detection if (t.bowser.gecko) { .. do something .. } // Rendering engine detection if (t.bowser.webkit || t.bowser.blink) { .. do something .. } * * This property has an alias - {@link #browser}. */ bowser : null, /** * @property {Object} bowser Alias for {@link #bowser} */ browser : null, forceDOMVisible : false, isDOMForced : false, browserInfo : { lazy : function () { return this.parseBrowser(window.navigator.userAgent) } }, nextConfirmValue : null, nextPromptReturnValue : null, realAlert : null, realConfirm : null, realPrompt : null, realPrint : null, realOpen : null, previousConfirm : null, previousPrompt : null, blurListener : null, restartOnBlur : false, blurWindow : null, mouseVisualizer : null, popups : Joose.I.Array, simulator : null, simulatorConfig : null, isHandlingThrowAsync : false, initialCursorPosition : null }, override : { cleanup : function () { if (this.mouseVisualizer) { this.mouseVisualizer.destroy() this.mouseVisualizer = null } this.SUPERARG(arguments) this.bowser = this.browser = null this._global = null this.realAlert = this.realConfirm = this.realPrompt = this.realPrint = this.realOpen = null this.previousPrompt = this.previousConfirm = null this.blurListener = null this.blurWindow && this.blurWindow.close(); this.blurWindow = null; Joose.A.each(this.popups, function (handle) { if (!handle.popup.closed) handle.popup.close() }) this.popups.length = 0 this.popups = null this.simulator.cleanup() }, attachSimulator : function () { var me = this var simulator = this.simulator var isRoot = !this.parent // this will "attach" simulator to a test window, by setting up a "global" property if (isRoot) simulator.onTestLaunch(this) if (simulator.type == 'synthetic') { // for synthetic simulator we can just update the current position directly (move to simulator class?) if (isRoot && me.initialCursorPosition) { simulator.currentPosition[ 0 ] = me.initialCursorPosition[ 0 ] simulator.currentPosition[ 1 ] = me.initialCursorPosition[ 1 ] } return } var cont = Promise.resolve() return cont.then(function () { if (isRoot) { var x = me.initialCursorPosition ? me.initialCursorPosition[ 0 ] : 0 var y = me.initialCursorPosition ? me.initialCursorPosition[ 1 ] : 0 // for native simulator we need to actually move the cursor return simulator.simulateMouseMove(x, y, null, { moveKind : 'instant' }) } }).then(function () { var hasFocus = me.project.browserWindowHasFocus() // browser window has lost focus for some reason, trying to re-focus by clicking // in -1, -1 point if (!hasFocus) { var currentPosition = simulator.currentPosition.slice() return simulator.simulateMouseMove(-1, -1, null, { moveKind : 'instant' }).then(function () { return simulator.simulateMouseClick({ globalXY : [] }) }).then(function () { return simulator.simulateMouseMove(currentPosition[ 0 ], currentPosition[ 1 ], null, { moveKind : 'instant' }) }) } }) }, launch : function (errorMessage) { var me = this var SUPER = this.SUPER var cont = this.attachSimulator() // "attachSimulator" completed synchronously - call SUPER immediately // this is required for "subTest" call to be synchronous on what some tests relies (subject for change) if (!cont) { this.SUPER(errorMessage) } else { // "attachSimulator" returned promise cont.then(function () { SUPER.call(me, errorMessage) }, function (reason) { SUPER.call(me, reason) }) } } }, methods : { initialize : function () { if (!this.simulator) this.simulator = new (this.getSimulatorClass())(this.simulatorConfig || {}) // copy the "currentPosition" to the test instance for backward compatibility this.currentPosition = this.simulator.currentPosition this.SUPERARG(arguments) }, onBeforeTestFinalize : function () { var global = this.global // If expectAlertMessage(which overwrites the alert method) was called but no alert() call happened - fail the test if (global.alert.__EXPECTED_ALERT__) { this.fail(Siesta.Resource('Siesta.Test.Browser', 'alertMethodNotCalled')) } this.SUPERARG(arguments) }, getSimulatorClass : function () { return Siesta.Test.Simulator }, // setup : function (callback, errback) { // var simulator = this.simulator // // // this will "attach" simulator to a test window, by setting up a "global" property // simulator.onTestLaunch(this) // // if (simulator instanceof Siesta.Test.Simulator) { // // synthetic events start with [ 0, 0 ] point anyway, so avoid extra mouseover/mousemove event // callback() // } else { // simulator.simulateMouseMove(0, 0, null, { moveKind : 'instant' }).then(callback, errback) // } // }, earlySetup : function (callback, errback) { var simulator = this.simulator if (simulator.type == 'synthetic') { // synthetic events start with [ 0, 0 ] point anyway, so avoid extra mouseover/mousemove event callback() } else // for native events need to reset the simulation state *before* the test starts // all keys up, mouse buttons up, cursor in 0, 0 simulator.doFullSimulationReset().then(callback, errback) }, launch : function (errorMessage) { var me = this var win = this.global // top test if (!me.parent) { me.realAlert = win.alert me.realConfirm = win.confirm me.realPrompt = win.prompt me.realPrint = win.print me.realOpen = win.open this.maintainScrollPositionDuring(function () { if (!me.project.browserWindowHasFocus() && !bowser.safari) me.onWindowBlur() // trying to focus the window (hopefully fixes the tab key issues) win.focus && win.focus() }) // win.addEventListener && win.addEventListener('blur', me.blurListener = function () { // if (bowser.gecko && win.document.getElementsByTagName('iframe').length > 0) // // this "waitFor" can be interrupted, but only by forceful test finalization, which // // happens when test throws exception for example, so it fails anyway // me.waitFor({ // method : 0, // suppressAssertion : true, // callback : function () { me.onWindowBlur() } // }) // else // me.onWindowBlur() // }) // 1. IE can't show cursor, since it's IE. // 2. Clients can opt-in for cursor display in automation mode // 3. Currently we don't show cursor for tests running in popups, since that would require // injecting cursor element on the test page and we don't want that if (!bowser.msie && me.project.showCursor && !win.opener) { me.mouseVisualizer = new Siesta.Project.Browser.UI.MouseVisualizer({ currentContainer : me.global.frameElement.parentElement }); } } // WARN: behavior when several sub-tests are running at the same time is not well-defined me.previousConfirm = win.confirm me.previousPrompt = win.prompt var emptyFn = function () {}; win.alert = win.print = emptyFn; win.confirm = function () { var retVal = typeof me.nextConfirmValue === 'boolean' ? me.nextConfirmValue : true; me.nextConfirmValue = null; return retVal; }; win.prompt = function () { var retVal = me.nextPromptReturnValue || ''; me.nextPromptReturnValue = null; return retVal; }; win.open = function (url) { var popup = me.realOpen.apply(win, arguments) if (!popup) me.fail(Siesta.Resource('Siesta.Test.Browser','popupsDisabled', { url : url })) else { me.popups.push({ url : url, popup : popup }) } return popup } this.SUPERARG(arguments) }, onTestFinalize : function () { var win = this.global if (win) { if (!this.parent) { win.confirm = this.previousConfirm; win.prompt = this.previousPrompt; win.print = this.realPrint win.alert = this.realAlert win.open = this.realOpen } else { win.confirm = this.realConfirm; win.prompt = this.realPrompt; win.alert = win.print = function () {} } } // this.blurListener && win.removeEventListener('blur', this.blurListener) // // this.blurListener = null this.SUPERARG(arguments) }, onWindowBlur : function (arg1, arg2) { // var doc = this.global.document // // // ignore the case when focus is moved inside of the child iframe // // IGNORE // if (!doc.hasFocus && doc.hasFocus()) return // // var slice = Array.prototype.slice // // // convert from HTMLCollection to Array // var iframes = slice.apply(doc.getElementsByTagName('iframe')) // // while (iframes.length) { // try { // var innerDoc = iframes[ 0 ].contentWindow.document // // if (innerDoc.hasFocus()) return // // iframes.push.apply(iframes, slice.apply(innerDoc.getElementsByTagName('iframe'))) // } catch (e) { // } // // iframes.shift() // } // // EOF IGNORE if (this.restartOnBlur) this.fireEvent('focuslost') else this.warn(Siesta.Resource('Siesta.Test.Browser').get('focusLostWarning', { url : this.url })) }, sizzle : function (selector, root) { return Siesta.Sizzle(selector, root || this.global.document) }, isEventPrevented : function (event) { // our custom property - takes highest priority if (event.preventDefault && this.typeOf(event.preventDefault.$prevented) == 'Boolean') return event.preventDefault.$prevented // W3C standards property if (this.typeOf(event.defaultPrevented) == 'Boolean') return event.defaultPrevented return event.returnValue === false }, // only called for the re-used contexts cleanupContextBeforeStart : function () { this.cleanupContextBeforeStartDom() this.SUPER() }, cleanupContextBeforeStartDom : function () { var doc = this.global.document doc.body.innerHTML = '' }, getElementPageRect : function (el, $el) { $el = $el || this.$(el) var offset = $el.offset() return new Siesta.Util.Rect({ left : offset.left, top : offset.top, width : $el.outerWidth(), height : $el.outerHeight() }) }, // TODO no longer used, remove after some time // elementHasScroller : function (el, $el) { // $el = $el || this.$(el) // // var overflowX = $el.css('overflow-x') // var overflowY = $el.css('overflow-y') // // var hasX = el.scrollWidth != el.clientWidth && overflowX != 'visible' && overflowX != 'hidden' // var hasY = el.scrollHeight != el.clientHeight && overflowY != 'visible' && overflowY != 'hidden' // // return hasX || hasY ? { x : hasX, y : hasY } : false // }, hasForcedIframe : function () { return Boolean( (this.isDOMForced || this.forceDOMVisible) && (this.scopeProvider instanceof Scope.Provider.IFrame) && this.scopeProvider.iframe ) }, getDivBox : function (doc, left, top, width, height) { var div = doc.createElement('div') div.style.cssText = 'position: absolute !important; left: ' + left + 'px !important; top: ' + top + 'px !important;' + 'border-width: 0 !important; padding: 0 !important; margin: 0 !important;' + 'width: ' + width + 'px !important; height: ' + height + 'px !important;' return div }, elementIsScrolledOut : function (el, offset) { var doc = el.ownerDocument var win = doc.defaultView || doc.parentWindow var body = this.getBodyElement(el) var parents = [] for (var parent = el; parent && parent !== body; parent = parent.parentNode) parents.push(parent) var currentRect = new Siesta.Util.Rect({ left : this.getPageScrollX(win), top : this.getPageScrollY(win), // using height / width of the *whole viewport*, BODY tag may have 0 height in some cases width : this.$(win).width(), height : this.$(win).height() }) for (var i = parents.length - 1; i >= 0; i--) { var parent = parents[ i ] var overflowX = this.$(parent).css('overflow-x') var overflowY = this.$(parent).css('overflow-y') if (overflowX !== 'visible' || overflowY !== 'visible') { // only get the bounding rect if we need it // in IE10 a series of call to `getBoundingClientRect` of the parent elements // were making the el itself hidden (offsetWidth = offsetHeight = 0) var parentRect = this.getElementPageRect(parent) if (overflowX !== 'visible') { currentRect = currentRect.cropLeftRight(parentRect) if (currentRect.isEmpty()) return true } if (overflowY !== 'visible') { currentRect = currentRect.cropTopBottom(parentRect) if (currentRect.isEmpty()) return true } } } var $el = this.$(el) var elPageRect = this.getElementPageRect($el[ 0 ], $el) offset = this.normalizeOffset(offset, $el) return !currentRect.contains(elPageRect.left + offset[ 0 ], elPageRect.top + offset[ 1 ]) }, // returns "true" if scrolling has actually occured scrollTargetIntoView : function (target, offset) { var win = this.global var doc = win.document if (this.typeOf(target) !== 'Array') { target = this.normalizeElement(target, true, null, false); offset = this.normalizeOffset(offset, this.$(target)) var isInside = this.isOffsetInsideElementBox(target, offset); if ( target && this.isElementVisible(target) && // If element isn't visible, try to bring it into view isInside && this.elementIsScrolledOut(target, offset) ) { this.maintainScrollPositionDuring(function () { // Required to handle the case where the body is scrolled target.scrollIntoView(); }) // No longer use jQuery "scrollIntoView" plugin - tests passes w/o it // and it does not take the offset point into account anyway // we still need it for ":scrollable" pseudo (which it does kind of ok) // this.$(target).scrollintoview({ duration : 0 }); // If element is still out of view, try manually scrolling first scrollable parent found if (this.elementIsScrolledOut(target, offset)) { var scrollableParent = this.$(target).closest(':scrollable')[ 0 ]; if (scrollableParent) { var parentBox = this.getBoundingClientRect(scrollableParent) var targetBox = this.getBoundingClientRect(target) scrollableParent.scrollLeft = Math.max(0, scrollableParent.scrollLeft + targetBox.left - parentBox.left + offset[ 0 ] - 1) scrollableParent.scrollTop = Math.max(0, scrollableParent.scrollTop + targetBox.top - parentBox.top + offset[ 1 ] - 1) } } return true } } else { var leftVisible = this.getPageScrollX() var rightVisible = leftVisible + this.$(win).width() var topVisible = this.getPageScrollY() var bottomVisible = topVisible + this.$(win).height() if ( leftVisible <= target[ 0 ] && target[ 0 ] <= rightVisible && topVisible <= target[ 1 ] && target[ 1 ] <= bottomVisible ) { // no need to scroll, target point is within visible viewport area return false } var div = this.getDivBox(doc, target[ 0 ], target[ 1 ], 1, 1) doc.body.appendChild(div) this.maintainScrollPositionDuring(function () { div.scrollIntoView() }) doc.body.removeChild(div) return true } }, processSubTestConfig : function (config) { var res = this.SUPER(config) var me = this Joose.A.each([ 'currentPosition', 'mouseVisualizer', 'simulator', 'simulatorConfig', 'bowser', 'browser', 'moveCursorBetweenPoints', 'realAlert', 'realConfirm', 'realPrompt', 'realPrint', 'realOpen', 'popups' ], function (name) { res[ name ] = me[ name ] }) return res }, // Normalizes the element to an HTML element. Every 'framework layer' will need to provide its own implementation // This implementation accepts either a CSS selector or an Array with xy coordinates. normalizeElement : function (el, allowMissing, shallow, detailed) { // Quick exit if already an element if (el && el.nodeName) return el; var matchingMultiple = false if (this.typeOf(el) === 'String') { var query = el; // DOM query var matches = this.query(el); el = matches[0]; matchingMultiple = matches.length > 1 if (!allowMissing && !el) { var warning = Siesta.Resource('Siesta.Test.Browser','noDomElementFound') + ': ' + query this.warn(warning); throw new Error(warning); } } if (this.typeOf(el) === 'Array') { var x, y if (el.length < 2) { x = this.simulator.currentPosition[ 0 ] y = this.simulator.currentPosition[ 1 ] } else { x = this.pageXtoViewportX(Math.round(el[ 0 ])) y = this.pageYtoViewportY(Math.round(el[ 1 ])) } el = this.elementFromPoint(x, y); } return detailed ? { el : el, matchingMultiple : matchingMultiple } : el; }, // this method generally has the same semantic as the "normalizeElement", its being used in // Siesta.Test.Action.Role.HasTarget to determine what to pass to next step // // on the browser level the only possibility is DOM element // but on ExtJS level user can also use ComponentQuery and next step need to receive the // component instance normalizeActionTarget : function (el, allowMissing) { return this.normalizeElement(el, allowMissing); }, randomBetween : function (min, max) { return Math.floor(min + (Math.random() * (max - min + 1))); }, /** * This method uses native `document.elementFromPoint()` and returns the DOM element under the current logical cursor * position in the test. Note, that this method may work not 100% reliable in IE due to its bugs. In cases * when `document.elementFromPoint()` can't find any element this method returns the <body> element. * * @return {HTMLElement} */ getElementAtCursor : function() { var xy = this.simulator.currentPosition; return this.elementFromPoint(xy[ 0 ], xy[ 1 ]) }, addListenerToObservable : function (observable, event, listener, isSingle) { if (this.typeOf(observable) === 'String') observable = this.normalizeElement(observable) observable.addEventListener(event, listener) }, removeListenerFromObservable : function (observable, event, listener) { if (this.typeOf(observable) === 'String') observable = this.normalizeElement(observable) observable.removeEventListener(event, listener) }, // This method accepts actionTargets as input (Dom node, string, CQ etc) and does a first // normalization pass to get a DOM element. // After initial normalization it also tries to locate, the 'top' DOM node at the center of // the first pass resulting DOM node. // This is the only element we can truly interact with in a real browser. // returns an object containing the element plus viewport coordinates getNormalizedTopElementInfo : function (actionTarget, skipWarning, actionName, offset) { var localXY, globalXY, el; // support empty array as a synonym for empty target if (this.typeOf(actionTarget) == 'Array' && actionTarget.length == 0) { actionTarget = null } var targetIsPoint = !actionTarget || this.typeOf(actionTarget) == 'Array' // First lets get a normal DOM element to work with if (targetIsPoint) { // viewport coords var x, y if (actionTarget) { x = this.pageXtoViewportX(Math.round(actionTarget[ 0 ])) y = this.pageYtoViewportY(Math.round(actionTarget[ 1 ])) } else { x = this.simulator.currentPosition[ 0 ] y = this.simulator.currentPosition[ 1 ] } globalXY = [ x, y ] var info = this.elementFromPoint(x, y, false, null, true) el = info.el localXY = info.localXY } else { el = this.normalizeElement(actionTarget, skipWarning) } if (!el && skipWarning) { return; } // 1. If this element is not visible, something is wrong // 2. If element is visible but not reachable (scrolled out of view) this is also an invalid scenario (this check is skipped for IE) // TODO needs further investigation, conflicting with starting a drag operation on an element that isn't visible until the cursor is above it // we don't need to this check if target is a coordinate point, because in this case element is reachable by definition if (!targetIsPoint) { var R = Siesta.Resource('Siesta.Test.Browser'); var message = 'getNormalizedTopElementInfo: ' + (actionName ? R.get('targetElementOfAction') + " [" + actionName + "]" : R.get('targetElementOfSomeAction')) + " " + R.get('isNotVisible') + ": " + (el.id ? '#' + el.id : el) if (!this.isElementVisible(el)) { this.fail(message) return; } else if (!skipWarning && this.isOffsetInsideElementBox(el, offset) && !this.elementIsTop(el, true, offset)) { this.warn(message) } } var isOption = el && el.nodeName.toLowerCase() === 'option'; if (isOption) { localXY = this.simulator.currentPosition.slice(); globalXY = this.simulator.currentPosition.slice(); } else if (!targetIsPoint) { var doc = this.getQueryableContainer(el); var R = Siesta.Resource('Siesta.Test.Browser'); localXY = this.getTargetCoordinate(el, true, offset) globalXY = this.getTargetCoordinate(el, false, offset) // trying 2 times for IE el = doc.elementFromPoint(localXY[ 0 ], localXY[ 1 ]) || doc.elementFromPoint(localXY[ 0 ], localXY[ 1 ]) || doc.body; if (!el) { this.fail('getNormalizedTopElementInfo: ' + R.get('noElementFound') + ' [' + localXY + ']'); return; // No point going further } } return { el : el, // both are viewport coords localXY : localXY, globalXY : globalXY, offset : isOption ? [ 0, 0 ] : this.getOffsetRelativeToEl(el, localXY) } }, // point should be in page coords getOffsetRelativeToEl : function(el, point) { var box = this.getElementPageRect(el); return [ point[0] - box.left, point[1] - box.top ]; }, /** * This method will wait for the presence of the passed string. * * @param {String} text The text to wait for * @param {Function} callback The callback to call * @param {Object} scope The scope for the callback * @param {Number} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value. */ waitForTextPresent : function (text, callback, scope, timeout) { var R = Siesta.Resource('Siesta.Test.Browser'); return this.waitFor({ method : function () { return this.global.document.body.innerText.indexOf(text) >= 0; }, callback : callback, scope : scope, timeout : timeout, assertionName : 'waitForTextPresent', description : ' ' + R.get('text') + ' "' + text + '" ' + R.get('toBePresent') }); }, /** * This method will wait for the absence of the passed string. * * @param {String} text The text to wait for * @param {Function} callback The callback to call * @param {Object} scope The scope for the callback * @param {Number} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value. */ waitForTextNotPresent : function (text, callback, scope, timeout) { var R = Siesta.Resource('Siesta.Test.Browser'); return this.waitFor({ method : function () { return this.global.document.body.innerText.indexOf(text) < 0; }, callback : callback, scope : scope, timeout : timeout, assertionName : 'waitForTextNotPresent', description : ' ' + R.get('text') + ' "' + text + '" ' + R.get('toNotBePresent') }); }, /** * Waits until the passed action target is detected. This can be a string such as a component query, CSS query or a composite query. * * @param {String/Siesta.Test.ActionTarget} target The target presence to wait for * @param {Function} callback The callback to call after the target 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. */ waitForTarget : function(target, callback, scope, timeout, offset) { var me = this; var R = Siesta.Resource('Siesta.Test.Browser'); return this.waitFor({ method : function () { var el = me.normalizeElement(target, true, true, false, { offset : offset }) // If user is aiming outside the target, we'll *not* use the offset while // detecting target presence since having a visible sized box will suffice if (el && offset && me.isElementVisible(el) && !me.isOffsetInsideElementBox(el, offset)) { return true; } return el && me.elementIsTop(el, true, offset) }, callback : callback, scope : scope, timeout : timeout, assertionName : 'waitForTarget', description : ' ' + R.get('target') + ' "' + target + '" ' + R.get('toAppear') }); }, /** * Sets a new size for the test iframe * * @param {Int} width The new width * @param {Int} height The new height */ setWindowSize : function(width, height, callback) { this.scopeProvider.setViewportSize(width, height); callback && callback.call(this); }, getJUnitClass : function () { var browserInfo = this.getBrowserInfo() browserInfo = browserInfo.name + browserInfo.version return browserInfo + ':' + this.SUPER() }, // a stub method for the Lite package screenshot : function (options, callback) { this.diag("Command: `screenshot` skipped - not running in Standard Package") this.processCallbackFromTest(callback, [ 'skipped' ], this) }, // a stub method for the Lite package screenshotElement : function (target, fileName, callback) { this.diag("Command: `screenshot` skipped - not running in Standard Package") this.processCallbackFromTest(callback, [ 'skipped' ], this) }, /** * setUrl Opens the url provided (make sure you use the {@link Siesta.Project.Browser#enablePageRedirect} option on the Harness when using this API method) * * @param {String} url The new url for current page * @param {Function} callback The callback to call after the page has been loaded * @param {Object} scope The scope for the callback */ setUrl : function(url, callback, scope) { if (!url) throw new Error('Must provide a valid URL'); var me = this; if (me.global.location.href !== url) { var baseUrl = this.scopeProvider.sourceURL || this.project.baseUrl; var absURl = this.project.absolutizeURL(url, baseUrl); me.waitForPageLoad(callback, scope); me.global.location.href = absURl; } else { callback.call(scope || me); } }, /** * Sets the hash value of the location object * * @param {String} url The new hash * @param {Function} callback * @param {Object} scope The scope for the callback */ setHash : function(hash, callback, scope) { var me = this; me.global.location.hash = hash; callback && callback(scope || me); }, /** * Expects an alert message with the specified text to be shown during the test. If no alert is called, * or the text doesn't match, a failed assertion will be added. * * @param {String/RegExp} message The expected alert message or a regular expression to match * @param callback Only used internally when this method is called in a t.chain command */ expectAlertMessage : function (message, callback) { var me = this var global = this.global var prevAlert = global.alert global.alert = function (msg) { var passed = me.typeOf(message) == 'RegExp' ? message.test(msg) : message == msg if (passed) me.pass("Expected alert message has been shown") else me.fail("Wrong alert message has been shown", { assertionName : 'expectAlertMessage', got : msg, gotDesc : "Message shown", need : message, needDesc : "Expected message" }) global.alert = prevAlert }; global.alert.__EXPECTED_ALERT__ = true this.processCallbackFromTest(callback, null, this) }, /** * Sets the confirm dialog return value for the next window.confirm() call. * * @param {Boolean} value The confirm dialog return value (true or false) * @param callback Only used internally when this method is called in a t.chain command * */ setNextConfirmReturnValue : function (value, callback) { this.nextConfirmValue = value; this.processCallbackFromTest(callback, null, this) }, /** * Sets the prompt dialog return value for the next window.prompt() call. * * @param {String} value The confirm dialog return value * @param callback Only used internally when this method is called in a t.chain command */ setNextPromptReturnValue : function (value, callback) { this.nextPromptReturnValue = value; this.processCallbackFromTest(callback, null, this) }, waitForAnimations : function(callback) { callback.call(this); }, popupHasStartedLoading : function (popup, initialUrl) { if (String(initialUrl).toLowerCase() != 'about:blank' && popup.location.href == 'about:blank') return false return true }, /** * Switches the target of all Siesta interactive commands (like "click/type" etc) to a different * window (usually a popup). You can use {@link #switchToMain} method to switch back to main window. * * @param {String/RegExp/Object/Window/HTMLIFrameElement} [win] A new window which should be a target for all interactive commands. * If this argument is specified as `null` a first opened popup is used. * Can be specified as the: * * - Window - A global window instance * - Object - Object with the following properties * - url : String/RegExp - The first popup, opened with matching url will be used * - title : String/RegExp - The first popup, opened with matching title will be used * - String - corresponds to the `title` property of the Object branch * * @param {Function} callback Function to call once the switch has complete (will also wait until the target page * completes loading) * * @return {Window} Previously active window reference */ switchTo : function (win, callback) { var me = this // In Chrome, when popup for some url is just created, it has "url" set to "about:blank" // after some time the url is set to the original value and load process begins // this opens a race condition - one can not reliably predict when the popup has completed loading // doing our best this.waitFor({ method : function () { for (var i = 0; i < me.popups.length; i++) if (!me.popupHasStartedLoading(me.popups[ i ].popup, me.popups[ i ].url)) return false return true }, suppressAssertion : true, callback : function () { var found if (!win) { Joose.A.each(this.popups, function (handle) { if (!handle.popup.closed) { found = handle.popup; return false } }) win = found } if (this.typeOf(win) == 'String') win = { title : win } if (this.typeOf(win) == 'Object') { found = null var regexp = win.title || win.url if (this.typeOf(regexp) == 'String') regexp = new RegExp('^' + this.escapeRegExp(regexp) + '$') Joose.A.each(this.popups, function (handle) { var popup = handle.popup if (!popup.closed) if ( win.url && regexp.test(popup.location.href) || win.title && regexp.test(popup.document && popup.document.title || '') ) { found = popup return false } }) win = found } if (!win || win.self != win) { this.fail("Can't resolve target win: " + win) this.processCallbackFromTest(callback, null, this) return } this.setGlobal(win) // This has to be revised properly in the "context" branch, idea is, that we switch to popup's implementation // of `setTimeout` for waiting, asyncing etc, because thats what really user expect // however in IE test just hangs // this.originalSetTimeout = win.setTimeout // this.originalClearTimeout = win.clearTimeout this.waitFor({ suppressAssertion : true, method : function () { return win.document && win.document.readyState == 'complete' }, callback : callback }) } }) return this.global }, setGlobal : function (newGlobal) { this.global = newGlobal this.simulator.global = newGlobal }, /** * Switches all interactive commands back to main test window. * * @param {Function} callback Function to call once the switch has complete. */ switchToMain : function (callback) { this.switchTo(this.scopeProvider.scope, callback) }, /** * Only useful along with {@link Siesta.Project.Browser.enablePageRedirect enablePageRedirect} option * * Wait for the page load to occur and runs the callback. The callback will receive a "window" object. * Should be used when you are doing a redirect / refresh of the test page: * * t.waitForPageLoad(function (window) { * ... * }) * * Note, that method obviously must be called before the new page has completed loading, otherwise it will * wait indefinitely and fail (since there will be no page load). So, to avoid the race conditions, one * should always start waiting for page load *before* the action, that causes it. * * Consider the following example (where click on the `>> #loginPanel button` trigger a page redirect): // this code does not reliably - it contains a race condition // in Chrome, page refresh may happen too fast (even synchronously), // so, by the time the `waitForPageLoad` action will start, the page load event will already happen // and `waitForPageLoad` will wait indefinitely { click : '>> #loginPanel button' }, { waitFor : 'PageLoad'} * // Need to start waiting first, and only then - click // we'll use "trigger" config of the `wait` action for that { waitFor : 'PageLoad', trigger : { click : '>> #loginPanel button' } } // or, same action using function step: function (next) { t.waitForPageLoad(next) t.click('>> #loginPanel button', function () {}) } * * @method * @member Siesta.Test.Browser */ waitForPageLoad : function (callback, scope) { var me = this var global = this.global var unloaded = false var description = Siesta.Resource('Siesta.Test.More').get('pageToLoad'); var onUnloadHandler = function () { global.removeEventListener('unload', onUnloadHandler) unloaded = true } global.addEventListener('unload', onUnloadHandler) this.chain( { description : description, waitFor : function () { return unloaded || me.global.document.readyState != 'complete' }}, function (next) { global.removeEventListener('unload', onUnloadHandler) global = null onUnloadHandler = null next() }, { description : description, waitFor : function () { return me.global.document.readyState == 'complete' } }, { waitFor : 50 }, function () { me.processCallbackFromTest(callback, [ me.global ], scope || me) } ) }, /** * This method will just call the `setTimeout` method from the scope of the test page. * * Usually you don't need to use it - you can just call `setTimeout`, but if your test scripts resides in the * separate context, you need to use this method for `setTimeout` functionality. See documentation for {@link Siesta.Project.Browser#enablePageRedirect enablePageRedirect} * option and <a href="#!/guide/cross_page_testing">Cross page testing</a> guide. * * @param {Function} func The function to call after specified `delay` * @param {Number} delay The time to wait (in ms) before calling the `func` * @return {Number} timeoutId The id of the timeout, can be passed to {@link #clearTimeout} to cancel the function execution. * * @method * @member Siesta.Test.Browser */ setTimeout : function (func, delay) { var pageSetTimeout = this.global.setTimeout pageSetTimeout(func, delay) }, /** * This method will just call the `clearTimeout` method from the scope of the test page. * * Usually you don't need to use it - you can just call `clearTimeout`, but if your test scripts resides in the * separate context, you need to use this method for `clearTimeout` functionality. See documentation for {@link Siesta.Project.Browser#enablePageRedirect enablePageRedirect} * option and <a href="#!/guide/cross_page_testing">Cross page testing</a> guide. * * @param {Number} timeoutId The id of the timeout, recevied from the {@link #setTimeout} call * * @method * @member Siesta.Test.Browser */ clearTimeout : function (id) { var pageClearTimeout = this.global.clearTimeout pageClearTimeout(id) }, /** * This method mimics the deactivation of a browser window by opening a new window. Useful if you want to test * how your application behaves when your application is not active. * * @method * @member Siesta.Test.Browser */ simulateDeactivateWindow : function(callback) { this.blurWindow = this.global.open('about:blank'); callback && callback.call(this); }, /** * This method activates the main browser window reverts focus to the window object of the test. * * @method * @member Siesta.Test.Browser */ simulateActivateWindow : function(callback) { this.blurWindow && this.blurWindow.close(); this.global.focus(); this.blurWindow = null; callback && callback.call(this); }, /** * Queries for elements using a CSS selector (optionally inside specific root element). At the generic browser level, the query is a CSS selector * (other framework specific layers, like Ext JS adds additional semantic). * * You can also target elements in nested iframes or web components. For that, split the query into two parts, delimited with the `->`. * The first part of the query should target an iframe element or a web component. The second part will be resolved inside the DOM of that iframe. * * ``` * await t.click('iframe.frame -> button'); // Targeting element inside an iframeee * await t.click('todo-app -> input[type=text]'); // Targeting element inside a web component * ``` * * The nesting level is not limited - you can target elements in the nested iframe of the nested iframe etc. * * ``` * await t.click('iframe.frame1 -> iframe.frame2 -> button'); * ``` * * A few extra CSS pseudo selectors are also supported: `:contains()` and `:textEquals()` which makes it * possible to query elements by their exact (textEquals) or partial (contains) textual content. * * ``` * await t.click('.iframe1 -> .iframe2 -> button:contains(Save)'); * ``` * * @param {String} selector A CSS selector * @param {Object} [root] The root element for the query (or shadow root) * @return {Array[Element]} */ query : function (selector, root) { selector = selector.trim(); root = root || this.getNestedRoot(selector); return root ? this.sizzle(selector.split('->').pop(), root) : []; }, getGlobal : function(docRoot) { return docRoot.defaultView || this.global; }, getNestedRoot : function(selector, root) { var parts = selector.split('->'); root = root || this.global.document; if (parts.length > 1) { var newContext = this.query(parts.shift(), root)[0]; // Go deeper? if (newContext) { var global = newContext.contentWindow ? newContext.contentWindow : this.global; // Make sure nested web component shadow root exists if (!newContext.contentWindow && !newContext.shadowRoot) { return null; } root = newContext.shadowRoot || global.document; if (parts.length > 1) { return this.getNestedRoot(parts.join('->'), root); } } else { root = null; } } return root; }, setExceptionHandlerOverrideFlag : function (value) { var test = this while (test) { test.isHandlingThrowAsync = value test = test.parent } }, /** * This assertion passes if an exception with certain message is thrown, during the time since this method has been called * and until the `done` function has been called. The `done` function is returned from this method. It should be called once * the code, that is expected to throw exception, has completed. `done` function can be called asynchronously. * * If `done` is not called for more than `timeout` time, this assertions finalizes forcefully. * * For example: * var done t.chain( function (next) { done = t.throwsOkAsync('oops', 'Correct exception thrown', 25000) next() }, // the exception is expected from the click handler { click : '.some-button' }, function (next) { done() next() }, ) * * See also {@link Siesta.Test.Browser#livesOkAsync} method. * * @param {String/RegExp} expected The regex against which to test the stringified exception, can be also a plain string * @param {String} [desc] The description of the assertion * @param {Number} [timeout] The timeout after which this assertion will be finalized forcefully. Default value is {@link Siesta.Project#defaultTimeout} * * @return {Function} A function which should be called when the code block, which is expected to throw the exception, is completed */ throwsOkAsync : function (expected, desc, timeout) { var R = Siesta.Resource('Siesta.Test.More') var me = this var exceptionThrown = false var doneCalled = false var prevOnError = this.global.onerror var done = function () { if (doneCalled) return doneCalled = true me.endAsync(async) if (!exceptionThrown) { me.fail(desc, { assertionName : 'throwsOkAsync', annotation : R.get('didntThrow') }) } me.setExceptionHandlerOverrideFlag(false) me.global.onerror = prevOnError if (supportsUnhandledRejections) { me.global.removeEventListener('unhandledrejection', unhandledRejectionListener) } // return true to suppress possible timeout failure, since `done` is also used as a errback for `beginAsync` return true } this.on('beforetestfinalize', function () { if (!doneCalled) done() }, null, { single : true }) var supportsUnhandledRejections = 'onunhandledrejection' in this.global var async = this.beginAsync(timeout, done) this.setExceptionHandlerOverrideFlag(true) var verifyErrorText = function (errorText) { if (me.typeOf(expected) == "RegExp") if (errorText.match(expected)) me.pass(desc, { descTpl : R.get('exMatchesRe'), expected : expected }) else me.fail(desc, { assertionName : 'throwsOkAsync', got : errorText, gotDesc : R.get('exceptionStringifiesTo'), need : expected, needDesc : R.get('needStringMatching') }) else if (errorText.indexOf(expected) != -1) me.pass(desc, { descTpl : R.get('exContainsSubstring'), expected : expected }) else me.fail(desc, { assertionName : 'throwsOkAsync', got : errorText, gotDesc : R.get('exceptionStringifiesTo'), need : expected, needDesc : R.get('needStringContaining') }) } this.global.onerror = function (msg, url, lineNumber, col, error) { exceptionThrown = true var errorText = String(error ? error : msg) verifyErrorText(errorText) // "hide" the exception return true } if (supportsUnhandledRejections) { var unhandledRejectionListener = function (event) { exceptionThrown = true verifyErrorText(String(event.reason)) } me.global.addEventListener('unhandledrejection', unhandledRejectionListener) } return done }, /** * This assertion passes if no exceptions are thrown, during the time since this method has been called * and until the `done` function has been called. The `done` function is returned from this method. It should be called once * the code, that is expected to not throw exceptions, has completed. `done` function can be called asynchronously. * * If `done` is not called for more than `timeout` time, this assertions finalizes forcefully. * * For example: * var done t.chain( function (next) { done = t.livesOkAsync('No exception thrown') next() }, // the exception is expected from the click handler { click : '.some-button' }, function (next) { done() next() } ) * * See also {@link Siesta.Test.Browser#throwsOkAsync} method. * * @param {String} [desc] The description of the assertion * @param {Number} [timeout] The timeout after which this assertion will be finalized forcefully. Default value is {@link Siesta.Project#defaultTimeout} * * @return {Function} A function which should be called when the code block, which is expected to now throw exceptions, is completed */ livesOkAsync : function (desc, timeout) { var R = Siesta.Resource('Siesta.Test.More') var me = this var exceptionThrown = false var doneCalled = false var prevOnError = this.global.onerror var done = function () { if (doneCalled) return doneCalled = true me.endAsync(async) if (!exceptionThrown) { me.pass(desc, { descTpl : R.get('fnDoesntThrow') }) } me.setExceptionHandlerOverrideFlag(false) me.global.onerror = prevOnError if (supportsUnhandledRejections) { me.global.removeEventListener('unhandledrejection', unhandledRejectionListener) } // return true to suppress possible timeout failure, since `done` is also used as a errback for `beginAsync` return true } this.on('beforetestfinalize', function () { if (!doneCalled) done() }, null, { single : true }) var supportsUnhandledRejections = 'onunhandledrejection' in this.global var async = this.beginAsync(timeout, done) this.setExceptionHandlerOverrideFlag(true) this.global.onerror = function (msg, url, lineNumber, col, error) { exceptionThrown = true var errorText = String(error ? error : msg) me.fail(desc, { assertionName : 'livesOk', annotation : R.get('fnThrew') + ': ' + errorText }) // "hide" the exception return true } if (supportsUnhandledRejections) { var unhandledRejectionListener = function (event) { exceptionThrown = true me.fail(desc, { assertionName : 'livesOk', annotation : 'Promise rejected: ' + event.reason }) } me.global.addEventListener('unhandledrejection', unhandledRejectionListener) } return done }, onException : function () { if (this.isHandlingThrowAsync) return true } } });