UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

691 lines (604 loc) 23.8 kB
/*! * OpenUI5 * (c) Copyright 2009-2021 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ /*global Math */ sap.ui.define([ 'sap/ui/Device', "sap/ui/thirdparty/jquery", 'sap/ui/test/_LogCollector', 'sap/ui/test/_OpaLogger', 'sap/ui/test/_ParameterValidator', 'sap/ui/test/_UsageReport', 'sap/ui/test/_OpaUriParameterParser', 'sap/ui/test/_ValidationParameters' ], function(Device, $, _LogCollector, _OpaLogger, _ParameterValidator, _UsageReport, _OpaUriParameterParser, _ValidationParameters) { "use strict"; /////////////////////////////// /// Privates /////////////////////////////// var oLogger = _OpaLogger.getLogger("sap.ui.test.Opa"), oLogCollector = _LogCollector.getInstance(), queue = [], context = {}, timeout = -1, oStopQueueOptions, oQueueDeferred, isEmptyQueueStarted, lastInternalWaitStack, oValidator = new _ParameterValidator({ errorPrefix: "sap.ui.test.Opa#waitFor" }); oLogCollector.start(); function internalWait (fnCallback, oOptions) { // Increase the wait timeout in debug mode, to allow debugging the waitFor without getting timeouts if (window["sap-ui-debug"]){ oOptions.timeout = oOptions.debugTimeout; } var startTime = new Date(); opaCheck(); function opaCheck () { oLogger.timestamp("opa.check"); oLogCollector.getAndClearLog(); var oResult = fnCallback(); lastInternalWaitStack = oOptions._stack; if (oResult.error) { oQueueDeferred.reject(oOptions); return; } if (oResult.result) { internalEmpty(); return; } var iPassedSeconds = (new Date() - startTime) / 1000; if (oOptions.timeout === 0 || oOptions.timeout > iPassedSeconds) { timeout = setTimeout(opaCheck, oOptions.pollingInterval); // OPA timeout not yet reached return; } // Timeout is reached and the check never returned true. // Execute the error function (if provided in the options) and reject the queue promise. addErrorMessageToOptions("Opa timeout after " + oOptions.timeout + " seconds", oOptions); if (oOptions.error) { try { oOptions.error(oOptions, oResult.arguments); } finally { oQueueDeferred.reject(oOptions); } } else { oQueueDeferred.reject(oOptions); } } } function internalEmpty () { if (!queue.length) { if (oQueueDeferred) { oQueueDeferred.resolve(); } return true; } var queueElement = queue.shift(); timeout = setTimeout(function () { internalWait(queueElement.callback, queueElement.options); }, (Opa.config.asyncPolling ? queueElement.options.pollingInterval : 0) + Opa.config.executionDelay); } function ensureNewlyAddedWaitForStatementsPrepended (oWaitForCounter, oNestedInOptions){ var iNewWaitForsCount = oWaitForCounter.get(); if (iNewWaitForsCount) { var aNewWaitFors = queue.splice(queue.length - iNewWaitForsCount, iNewWaitForsCount); aNewWaitFors.forEach(function(queueElement) { queueElement.options._nestedIn = oNestedInOptions; }); queue = aNewWaitFors.concat(queue); } } function getMessageForException (oError) { var sExceptionText = oError.toString(); // Some browsers don't have the stack property it will be added later for those browsers if (oError.stack) { sExceptionText += "\n" + oError.stack; } var sErrorMessage = "Exception thrown by the testcode:'" + sExceptionText + "'"; return sErrorMessage; } function addErrorMessageToOptions (sErrorMessage, oOptions, oErrorStack) { var sLogs = oLogCollector.getAndClearLog(); if (sLogs) { sErrorMessage += "\nThis is what Opa logged:\n" + sLogs; } if (!oErrorStack && oOptions._stack) { // if we do not have a stack in the exception (IE) manually add it sErrorMessage += addStacks(oOptions); } if (oOptions.errorMessage) { oOptions.errorMessage += "\n" + sErrorMessage; } else { oOptions.errorMessage = sErrorMessage; } oLogger.error(oOptions.errorMessage, "Opa"); } function createStack (iDropCount) { iDropCount = (iDropCount || 0) + 2; if (Device.browser.mozilla) { //firefox needs one less in the string iDropCount = iDropCount - 1; } var oError = new Error(), stack = oError.stack; if (!stack){ //In IE an error has to be thrown first to get a stack try { throw oError(); } catch (oError2) { stack = oError2.stack; } } // IE <= 9 this will not work if (!stack) { return ""; } stack = stack.split("\n"); stack.splice(0, iDropCount); return stack.join("\n"); } function addStacks (oOptions) { var sResult = "\nCallstack:\n"; if (oOptions._stack) { sResult += oOptions._stack; delete oOptions._stack; } else { sResult += "Unknown"; } if (oOptions._nestedIn) { sResult += addStacks(oOptions._nestedIn); delete oOptions._nestedIn; } return sResult; } /////////////////////////////// /// Public /////////////////////////////// /** * This class will help you write acceptance tests in one page or single page applications. * You can wait for certain conditions to be met. * * @class One Page Acceptance testing. * @public * @alias sap.ui.test.Opa * @author SAP SE * @since 1.22 * * @param {object} [extensionObject] An object containing properties and functions. The newly created Opa will be extended by these properties and functions using jQuery.extend. */ var Opa = function(extensionObject) { this.and = this; $.extend(this, extensionObject); }; /** * The global configuration of Opa. * All of the global values can be overwritten in an individual <code>waitFor</code> call. * The default values are: * <ul> * <li>arrangements: A new Opa instance</li> * <li>actions: A new Opa instance</li> * <li>assertions: A new Opa instance</li> * <li>timeout : 15 seconds, 0 for infinite timeout</li> * <li>pollingInterval: 400 milliseconds</li> * <li>debugTimeout: 0 seconds, infinite timeout by default. This will be used instead of timeout if running in debug mode.</li> * <li>asyncPolling: false</li> * </ul> * You can either directly manipulate the config, or extend it using {@link sap.ui.test.Opa.extendConfig}. * @public */ Opa.config = {}; /** * Extends and overwrites default values of the {@link sap.ui.test.Opa sap.ui.test.Opa.config} field. * Sample usage: * <pre> * <code> * var oOpa = new Opa(); * * // this statement will time out after 15 seconds and poll every 400ms * // those two values come from the defaults of sap.ui.test.Opa.config * oOpa.waitFor({ * }); * * // All wait for statements added after this will take other defaults * Opa.extendConfig({ * timeout: 10, * pollingInterval: 100 * }); * * // this statement will time out after 10 seconds and poll every 100 ms * oOpa.waitFor({ * }); * * // this statement will time out after 20 seconds and poll every 100 ms * oOpa.waitFor({ * timeout: 20; * }); * </code> * </pre> * * @since 1.40 The own properties of 'arrangements, actions and assertions' will be kept. * Here is an example: * <pre> * <code> * // An opa action with an own property 'clickMyButton' * var myOpaAction = new Opa(); * myOpaAction.clickMyButton = // function that clicks MyButton * Opa.config.actions = myOpaAction; * * var myExtension = new Opa(); * Opa.extendConfig({ * actions: myExtension * }); * * // The clickMyButton function is still available - the function is logged out * console.log(Opa.config.actions.clickMyButton); * * // If * var mySecondExtension = new Opa(); * mySecondExtension.clickMyButton = // a different function than the initial one * Opa.extendConfig({ * actions: mySecondExtension * }); * * // Now clickMyButton function is the function of the second extension not the first one. * console.log(Opa.config.actions.clickMyButton); * </code> * </pre> * * @since 1.48 All config parameters could be overwritten from URL. Should be prefixed with 'opa' * and have uppercase first character. Like 'opaExecutionDelay=1000' will overwrite 'executionDelay' * * @param {object} options The values to be added to the existing config * @public */ Opa.extendConfig = function (oOptions) { var aComponents = ["actions", "assertions", "arrangements"]; aComponents.filter(function (sArrangeActAssert) { return !!oOptions[sArrangeActAssert]; }).forEach(function (sArrangeActAssert) { // actions, assertions and arrangements are objects of a type that extends OPA // this means that somewhere along the prototype chain, .__proto__ will be either OPA or OPA5 // this is necessary for chaining in test journeys (".and") var oNewComponent = oOptions[sArrangeActAssert]; var oNewComponentProto = Object.getPrototypeOf(oOptions[sArrangeActAssert]); var oCurrentConfig = Opa.config[sArrangeActAssert]; var oCurrentConfigProto = Object.getPrototypeOf(Opa.config[sArrangeActAssert]); // in order to merge new and existing components and preserve the prototype of the new component, // add existing component properties to the new component for (var sKey in oCurrentConfig) { if (!(sKey in oNewComponent)) { oNewComponent[sKey] = oCurrentConfig[sKey]; } } for (var sProtoKey in oCurrentConfigProto) { if (!(sProtoKey in oNewComponent)) { oNewComponentProto[sProtoKey] = oCurrentConfigProto[sProtoKey]; } } }); // URI params overwrite other config params // if any action, assertion or arrangement is already defined in OPA, it will be overwritten // deep extend is necessary so plain object configs like appParams are properly merged Opa.config = $.extend(true, Opa.config, oOptions, Opa._uriParams); _OpaLogger.setLevel(Opa.config.logLevel); }; // These browsers are not executing Promises as microtasks so slow down OPA a bit to let mircotasks before other tasks. // TODO: A proper solution would be waiting for all the active timeouts in the synchronization part until then this is a workaround // TODO: Workaround for IE with the IFrame startup. Without the frame the timeout can probably be 0 but this need to be evaluated as soon as we have an alternative startup // This has to be here for IFrame with IE - if there is no timeout 50, there is a window with all properties undefined. // Therefore the core code throws exceptions, when functions like setTimeout are called. // I don't have a proper explanation for this. var executionDelayDefault = 0; if (Device.browser.msie || Device.browser.edge || Device.browser.safari) { executionDelayDefault = 50; } /** * Reset Opa.config to its default values. * All of the global values can be overwritten in an individual waitFor call. * * The default values are: * <ul> * <li>arrangements: A new Opa instance</li> * <li>actions: A new Opa instance</li> * <li>assertions: A new Opa instance</li> * <li>timeout : 15 seconds, 0 for infinite timeout</li> * <li>pollingInterval: 400 milliseconds</li> * <li>debugTimeout: 0 seconds, infinite timeout by default. This will be used instead of timeout if running in debug mode.</li> * <li> * executionDelay: 0 or 50 (depending on the browser). The value is a number representing milliseconds. * The executionDelay will slow down the execution of every single waitFor statement to be delayed by the number of milliseconds. * This does not effect the polling interval it just adds an initial pause. * Use this parameter to slow down OPA when you want to watch your test during development or checking the UI of your app. * It is not recommended to use this parameter in any automated test executions. * </li> * <li>asyncPolling: false</li> * </ul> * * @public * @since 1.25 */ Opa.resetConfig = function () { Opa.config = $.extend({ arrangements : new Opa(), actions : new Opa(), assertions : new Opa(), timeout : 15, pollingInterval : 400, debugTimeout: 0, _stackDropCount : 0, //Internal use. Specify numbers of additional stack frames to remove for logging executionDelay: executionDelayDefault, asyncPolling: false }, Opa._uriParams); }; /** * Gives access to a singleton object you can save values in. * Same as {@link sap.ui.test.Opa#getContext} * @since 1.29.0 * @returns {object} the context object * @public * @function */ Opa.getContext = function () { return context; }; /** * Waits until all waitFor calls are done. * * @returns {jQuery.promise} If the waiting was successful, the promise will be resolved. If not it will be rejected * @public */ Opa.emptyQueue = function emptyQueue () { if (isEmptyQueueStarted) { throw new Error("Opa is emptying its queue. Calling Opa.emptyQueue() is not supported at this time."); } isEmptyQueueStarted = true; oStopQueueOptions = null; oQueueDeferred = $.Deferred(); internalEmpty(); return oQueueDeferred.promise().fail(function (oOptions) { queue = []; if (oStopQueueOptions) { var sErrorMessage = oStopQueueOptions.qunitTimeout ? "QUnit timeout after " + oStopQueueOptions.qunitTimeout + " seconds" : "Queue was stopped manually"; // if the queue was running, log the stack of the last executed check before the queue was stopped oOptions._stack = oStopQueueOptions.qunitTimeout && lastInternalWaitStack || createStack(1); addErrorMessageToOptions(sErrorMessage, oOptions); } }).always(function () { queue = []; timeout = -1; oQueueDeferred = null; lastInternalWaitStack = null; isEmptyQueueStarted = false; }); }; /** * Clears the queue and stops running tests so that new tests can be run. * This means all waitFor statements registered by {@link sap.ui.test.Opa#waitFor} will not be invoked anymore and * the promise returned by {@link sap.ui.test.Opa.emptyQueue} will be rejected * When it is called inside of a check in {@link sap.ui.test.Opa#waitFor} * the success function of this waitFor will not be called. * @since 1.40.1 * @public */ Opa.stopQueue = function stopQueue () { Opa._stopQueue(); }; Opa._stopQueue = function (oOptions) { // clear queue queue = []; if (!oQueueDeferred) { oLogger.warning("stopQueue was called before emptyQueue, queued tests have never been executed", "Opa"); } else { // clear running internalWait poll if (timeout !== -1) { clearTimeout(timeout); } oStopQueueOptions = oOptions || {}; oQueueDeferred.reject(oStopQueueOptions); } }; Opa._uriParams = _OpaUriParameterParser._getOpaParams(); //create the default config Opa.resetConfig(); Opa._usageReport = new _UsageReport(Opa.config); // set the maximum level for OPA logs _OpaLogger.setLevel(Opa.config.logLevel); /** * A map of QUnit-style assertions to be used in an opaTest. * Contains all methods available on QUnit.assert for the running QUnit version. * Available assertions are: ok, equal, propEqual, deepEqual, strictEqual and their negative counterparts. * * For more information, see {@link module:sap/ui/test/opaQunit}. * * @name sap.ui.test.Opa.assert * @public * @static * @type QUnit.Assert */ Opa.prototype = { /** * Gives access to a singleton object you can save values in. * This object will only be created once and it will never be destroyed. * That means you can use it to save values you need in multiple separated tests. * * @returns {object} the context object * @public * @function */ getContext : Opa.getContext, /** * Queues up a waitFor command for Opa. * The Queue will not be emptied until {@link sap.ui.test.Opa.emptyQueue} is called. * If you are using {@link module:sap/ui/test/opaQunit}, emptyQueue will be called by the wrapped tests. * * If you are using Opa5, waitFor takes additional parameters. * They can be found here: {@link sap.ui.test.Opa5#waitFor}. * Waits for a check condition to return true, in which case a success function will be called. * If the timeout is reached before the check returns true, an error function will be called. * * * @public * @param {object} options These contain check, success and error functions * @param {int} [options.timeout] default: 15 - (seconds) Specifies how long the waitFor function polls before it fails.O means it will wait forever. * @param {int} [options.debugTimeout] @since 1.47 default: 0 - (seconds) Specifies how long the waitFor function polls before it fails in debug mode.O means it will wait forever. * @param {int} [options.pollingInterval] default: 400 - (milliseconds) Specifies how often the waitFor function polls. * @param {boolean} [options.asyncPolling] @since 1.55 default: false Enable asynchronous polling after success() call. This allows more stable autoWaiter synchronization with event flows originating from within success(). Especially usefull to stabilize synchronization with overflow toolbars. * @param {function} [options.check] Will get invoked in every polling interval. * If it returns true, the check is successful and the polling will stop. * The first parameter passed into the function is the same value that gets passed to the success function. * Returning something other than boolean in the check will not change the first parameter of success. * @param {function} [options.success] Will get invoked after the check function returns true. * If there is no check function defined, it will be directly invoked. * waitFor statements added in the success handler will be executed before previously added waitFor statements. * @param {string} [options.errorMessage] Will be displayed as an errorMessage depending on your unit test framework. * Currently the only adapter for Opa is QUnit. * This message is displayed there if Opa has reached its timeout but QUnit has not yet reached it. * * @returns {object} an object extending a jQuery promise. * The object is essentially a jQuery promise with an additional "and" method that can be used for chaining waitFor statements. * The promise is resolved when the waitFor completes successfully. * The promise is rejected with the options object, if an error occurs. In this case, options.errorMessage will contain a detailed error message containing the stack trace and Opa logs. */ waitFor : function (options) { var deferred = $.Deferred(), oFilteredConfig = Opa._createFilteredConfig(Opa._aConfigValuesForWaitFor); options = $.extend({}, oFilteredConfig, options); this._validateWaitFor(options); options._stack = createStack(1 + options._stackDropCount); delete options._stackDropCount; // create a new deferred for each new queue element and decorate a copy of this which will be returned in the end // this way a promise result handler can be attached to any waitFor statement at any time var _this = $.extend({}, this); deferred.promise(_this); queue.push({ callback : function () { // check is truthy if there is no check function var bCheckPassed = true; if (options.check) { try { bCheckPassed = options.check.apply(this, arguments); } catch (oError) { var sErrorMessage = "Failure in Opa check function\n" + getMessageForException(oError); addErrorMessageToOptions(sErrorMessage, options, oError.stack); deferred.reject(options); return {error: true, arguments: arguments}; } } // if queue is stopped in the check function, don't execute success function and stop internalWait if (oStopQueueOptions) { return {result: true, arguments: arguments}; } if (!bCheckPassed) { return {result: false, arguments: arguments}; } if (options.success) { var oWaitForCounter = Opa._getWaitForCounter(); try { options.success.apply(this, arguments); } catch (oError) { var sErrorMessage = "Failure in Opa success function\n" + getMessageForException(oError); addErrorMessageToOptions(sErrorMessage, options, oError.stack); deferred.reject(options); return {error: true, arguments: arguments}; } finally { ensureNewlyAddedWaitForStatementsPrepended(oWaitForCounter, options); } } // check and success are OK deferred.resolve(); return {result: true, arguments: arguments}; }.bind(this), options : options }); return _this; }, /** * Calls the static extendConfig function in the Opa namespace {@link sap.ui.test.Opa.extendConfig} * @public * @function */ extendConfig : Opa.extendConfig, /** * Calls the static emptyQueue function in the Opa namespace {@link sap.ui.test.Opa.emptyQueue} * @public * @function */ emptyQueue : Opa.emptyQueue, /** * Schedule a promise on the OPA queue.The promise will be executed in order with all waitFors - * any subsequent waitFor will be executed after the promise is done. * The promise is not directly chained, but instead its result is awaited in a new waitFor statement. * This means that any "thenable" should be acceptable. * @public * @param {jQuery.promise|Promise} oPromise promise to schedule on the OPA queue * @returns {jQuery.promise} promise which is the result of a {@link sap.ui.test.Opa.waitFor} */ iWaitForPromise: function (oPromise) { return this._schedulePromiseOnFlow(oPromise); }, _schedulePromiseOnFlow: function (oPromise, oOptions) { // as the waitFor flow is driven by the polling, the only way to schedule // a promise on it is to insert a waitFor that polls the result. // an promised-based way will require a full rework of the flow management oOptions = oOptions || {}; var mPromiseState = {}; oOptions.check = function() { if (!mPromiseState.started) { mPromiseState.started = true; oPromise.then(function () { mPromiseState.done = true; }, function (error) { mPromiseState.errorMessage = "Error while waiting for promise scheduled on flow" + (error ? ", details: " + error : ""); }); } if (mPromiseState.errorMessage) { throw new Error(mPromiseState.errorMessage); } else { return !!mPromiseState.done; } }; return this.waitFor(oOptions); }, _validateWaitFor: function (oParameters) { oValidator.validate({ validationInfo: _ValidationParameters.OPA_WAITFOR, inputToValidate: oParameters }); } }; Opa._createFilteredOptions = function (aAllowedProperties, oSource) { var oFilteredOptions = {}; aAllowedProperties.forEach(function (sKey) { var vConfigValue = oSource[sKey]; if (vConfigValue === undefined) { return; } oFilteredOptions[sKey] = vConfigValue; }); return oFilteredOptions; }; Opa._createFilteredConfig = function (aAllowedProperties) { return Opa._createFilteredOptions(aAllowedProperties, Opa.config); }; Opa._getWaitForCounter = function () { var iQueueLengthOnCreation = queue.length; return { get: function () { var iLength = queue.length - iQueueLengthOnCreation; // never return negative numbers return Math.max(iLength, 0); } }; }; /* config values from opa.config that will be used in waitFor */ Opa._aConfigValuesForWaitFor = Object.keys(_ValidationParameters.OPA_WAITFOR_CONFIG); return Opa; }, /* export= */ true);