UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

232 lines (202 loc) 7.61 kB
/*! * OpenUI5 * (c) Copyright 2026 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ sap.ui.define([ "sap/base/util/extend", "sap/ui/test/autowaiter/_utils", "./WaiterBase" ], function(extend, _utils, WaiterBase) { "use strict"; var mTimeouts = {}; var timeoutStatus = { TRACKED: "TRACKED", STARTER: "STARTED", FINISHED: "FINISHED", CLEARED: "CLEARED" }; var fnInitiatorResolver; // initiatorId is the timeout id of the currently running timeout callback // for opa poll frame, will have the ID of the poll timeout // undefined means this is native event frame var iInitiatorId; var TimeoutWaiter = WaiterBase.extend("sap.ui.test.autowaiter._timeoutWaiter", { hasPending: function () { var aBlockingTimeoutIds = Object.keys(mTimeouts).filter(function (iID) { return isBlocking(iID); }); var bHasBlockingTimeouts = aBlockingTimeoutIds.length > 0; logTrackedTimeouts(aBlockingTimeoutIds); return bHasBlockingTimeouts; }, _getDefaultConfig: function () { return extend({ maxDepth: 1, // count maxDelay: 1000, // milliseconds minDelay: 10 // milliseconds }, WaiterBase.prototype._getDefaultConfig.call(this)); }, _getValidationInfo: function () { return extend({ maxDepth: "numeric", maxDelay: "numeric", minDelay: "numeric" }, WaiterBase.prototype._getValidationInfo.call(this)); }, // private API used by the proimiseWaiter for detecting polling promises // return the current execution timeoutId or undefined if not currently in tracked timeout callback _getInitiatorId: function() { return iInitiatorId; }, _isPolling: function(timeoutId) { return !isExecutionFlow(timeoutId); }, _registerInitiatorResolverId: function(fnInitiatorResolverCallback) { fnInitiatorResolver = fnInitiatorResolverCallback; } }); var oTimeoutWaiter = new TimeoutWaiter(); function _resolveInitiatorId() { if (iInitiatorId) { return iInitiatorId; } if (fnInitiatorResolver) { return fnInitiatorResolver(); } } function createTimeoutWrapper (sName) { var sSetName = "set" + sName; var sClearName = "clear" + sName; var fnOriginal = window[sSetName]; // set immediate is not standard if (!fnOriginal) { return; } var fnOriginalClear = window[sClearName]; window[sSetName] = function wrappedSetTimeout(fnCallback, iDelay, tracking) { iDelay = iDelay || 0; var aCallbackArgs = Array.prototype.slice.call(arguments, 2); var oNewTimeout = { delay: iDelay, initiator: _resolveInitiatorId(), func: _utils.functionToString(fnCallback), stack: _utils.resolveStackTrace(), status: timeoutStatus.TRACKED }; var iID; // some timeouts do not need to be tracked, like the timeout for long-running promises if (tracking && tracking === 'TIMEOUT_WAITER_IGNORE') { iID = fnOriginal.apply(null, [fnCallback, iDelay].concat(aCallbackArgs.slice(1))); oTimeoutWaiter._oLogger.trace("Timeout with ID " + iID + " should not be tracked. " + " Delay: " + iDelay + " Initiator: " + iInitiatorId); return iID; } var fnWrappedCallback = function wrappedCallback() { // workaround for FF: the mTimeouts[iID] is sometimes cleaned by GC before it is released var oCurrentTimeout = mTimeouts[iID]; if (!oCurrentTimeout) { oTimeoutWaiter._oLogger.trace("Timeout data for timeout with ID " + iID + " disapered unexpectedly"); oCurrentTimeout = {}; } iInitiatorId = iID; oTimeoutWaiter._oLogger.trace("Timeout with ID " + iID + " started"); oCurrentTimeout.status = timeoutStatus.STARTED; try { fnCallback.apply(window, aCallbackArgs); } finally { iInitiatorId = undefined; } oTimeoutWaiter._oLogger.trace("Timeout with ID " + iID + " finished"); oCurrentTimeout.status = timeoutStatus.FINISHED; }; iID = fnOriginal.apply(null, [fnWrappedCallback, iDelay].concat(aCallbackArgs)); oTimeoutWaiter._oLogger.trace("Timeout with ID " + iID + " is tracked. " + " Delay: " + iDelay + " Initiator: " + iInitiatorId); mTimeouts[iID] = oNewTimeout; return iID; }; window[sClearName] = function wrappedClearTimeout(iID) { if (!iID) { oTimeoutWaiter._oLogger.trace("Could not clean timeout with invalid ID: " + iID); return; } var oCurrentTimeout = mTimeouts[iID]; if (!oCurrentTimeout) { oTimeoutWaiter._oLogger.trace("Timeout data for timeout with ID " + iID + " disapered unexpectedly or timeout was not tracked intentionally"); oCurrentTimeout = {}; } oCurrentTimeout.status = timeoutStatus.CLEARED; oTimeoutWaiter._oLogger.trace("Timeout with ID " + iID + " cleared"); fnOriginalClear(iID); }; } createTimeoutWrapper("Timeout"); createTimeoutWrapper("Immediate"); function createLogForTimeout(iTimeoutID, oTimeout,bBlocking,bDetails) { return "\nTimeout: ID: " + iTimeoutID + " Type: " + (bBlocking ? "BLOCKING" : "NOT BLOCKING") + " Status: " + oTimeout.status + " Delay: " + oTimeout.delay + " Initiator: " + oTimeout.initiator + (bDetails ? ("\nFunction: " + oTimeout.func) : "") + (bDetails ? ("\nStack: " + oTimeout.stack) : ""); } function logTrackedTimeouts(aBlockingTimeoutIds) { var aTimeoutIds = Object.keys(mTimeouts); // log overview of blocking timeouts at debug var sLogMessage = "Found " + aBlockingTimeoutIds.length + " blocking out of " + aTimeoutIds.length + " tracked timeouts"; aBlockingTimeoutIds.forEach(function (iTimeoutID) { sLogMessage += createLogForTimeout(iTimeoutID, mTimeouts[iTimeoutID],aBlockingTimeoutIds.some(function(currentValue){ return currentValue == iTimeoutID; }),true); }); // show the pending timeout details into the timeout message oTimeoutWaiter._oHasPendingLogger.debug(sLogMessage); // log all tracked timeouts at trace var sTraceLogMessage = "Tracked timeouts"; aTimeoutIds.forEach(function (iTimeoutID) { sTraceLogMessage += createLogForTimeout(iTimeoutID, mTimeouts[iTimeoutID],aBlockingTimeoutIds.some(function(currentValue){ return currentValue == iTimeoutID; }),true); }); oTimeoutWaiter._oHasPendingLogger.trace(sTraceLogMessage); } function isBlocking(iID) { var oCurrentTimeout = mTimeouts[iID]; // we do not care for finished timeouts if (oCurrentTimeout.status !== timeoutStatus.TRACKED){ return false; } // long runnes are some application level timeouts => we do not care for them if (oCurrentTimeout.delay > oTimeoutWaiter._mConfig.maxDelay) { return false; } // zero or up to some small delay timeouts are definitely execution flow so must be waited if (oCurrentTimeout.delay > oTimeoutWaiter._mConfig.minDelay) { return isExecutionFlow(iID); } // all that are left should be waited for return true; } // analyse recursively if this timeout is either execution flow or polling function isExecutionFlow(currentId,depth) { depth = depth || 1; var oCurrentTimeout = mTimeouts[currentId]; // initiator could be untracked or lost so consider as flow if (oCurrentTimeout.initiator && mTimeouts[oCurrentTimeout.initiator]) { // if the initiator has the same timeout => check recursively for its parrent if (oCurrentTimeout.delay == mTimeouts[oCurrentTimeout.initiator].delay) { // if maxDepth chain has equal delays => this is a poll chain if (depth >= oTimeoutWaiter._mConfig.maxDepth) { return false; } return isExecutionFlow(oCurrentTimeout.initiator,depth + 1); } } return true; } return oTimeoutWaiter; }, true);