UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

435 lines (384 loc) 14 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/Log", "sap/base/util/ObjectPath", 'sap/ui/Device', 'sap/ui/test/_LogCollector', "sap/ui/thirdparty/jquery" ], function (Log, ObjectPath, Device, _LogCollector, jQuery) { "use strict"; /*global CollectGarbage */ // after CSS transform - scale down by 0.6: width=768, height=614.4 var DEFAULT_WIDTH = 1280; var DEFAULT_HEIGHT = 1024; var oFrameWindow = null, $Frame = null, $FrameContainer = null, oFramePlugin = null, oFrameUtils = null, oFrameJQuery = null, bRegisteredToUI5Init = false, bUi5Loaded = false, oAutoWaiter = null, FrameHashChanger = null, sOpaLogLevel, bDisableHistoryOverride; /* * INTERNALS */ function handleFrameLoad () { oFrameWindow = $Frame[0].contentWindow; registerOnError(); //immediately check for UI5 to be loaded, to intercept when the hash changes checkForUI5ScriptLoaded(); } function setFrameSize(sWidth, sHeight) { // by default the frame is scaled down to 60% of a fixed page size: 1280x1024 // user-defined dimensions should not be scaled if (sWidth) { $Frame.css("width", sWidth); $FrameContainer.css("padding-left", sWidth); } else { $Frame.css("width", DEFAULT_WIDTH); $Frame.addClass("default-scale-x"); } if (sHeight) { $Frame.css("height", sHeight); } else { $Frame.css("height", DEFAULT_HEIGHT); $Frame.addClass("default-scale-y"); } if (!sWidth && !sHeight) { $Frame.addClass("default-scale-both"); } } function registerOnError () { var fnFrameOnError = oFrameWindow.onerror; oFrameWindow.onerror = function (sErrorMsg, sUrl, iLine, iColumn, oError) { var vReturnValue = false; if (fnFrameOnError) { // save the return value if the original returns true - the error is supressed vReturnValue = fnFrameOnError.apply(this, arguments); } // a global exception in the outer window's scope should be fired. but since this onerror // function is wrapped in QUnits onerror function the exception needs to be thrown in a setTimeout // to make sure the QUnit onerror can run to the end setTimeout(function () { // column number and error object may be missing in older browsers. Currently, Edge doesn't provide the oError object var sColumn = iColumn ? "\ncolumn: " + iColumn : ""; var sIFrameError = oError && "\niFrame error: " + (oError.stack ? oError.message + "\n" + oError.stack : oError) || ""; throw new Error("Error in launched application iFrame: " + sErrorMsg + "\nurl: " + sUrl + "\nline: " + iLine + sColumn + sIFrameError); }, 0); return vReturnValue; }; } function checkForUI5ScriptLoaded () { if (bUi5Loaded) { return true; } if (!bRegisteredToUI5Init && oFrameWindow?.sap?.ui) { if ( oFrameWindow.sap.ui.require ) { // use the more modern Core.ready, if available const oFrameCore = oFrameWindow.sap.ui.require("sap/ui/core/Core"); if (typeof oFrameCore?.ready === "function") { oFrameCore.ready(handleUi5Loaded); bRegisteredToUI5Init = true; return bUi5Loaded; } // otherwise, fall back to the older sap.ui.getCore().attachInit if (typeof oFrameCore?.attachInit === "function") { oFrameCore.attachInit(handleUi5Loaded); bRegisteredToUI5Init = true; } } } return bUi5Loaded; } /** * Firefox only function - load sinon as often as needed until it is defined. * @param {function} fnDone executed when sinon is loaded */ function loadSinon(fnDone) { oFrameWindow.sap.ui.require(["sap/ui/thirdparty/sinon"], function (sinon) { if (!sinon) { setTimeout(function () { loadSinon(fnDone); }, 50); } else { fnDone(); } }); } function handleUi5Loaded () { registerFrameResourcePaths(); if (Device.browser.firefox) { // In Firefox there is a bug when the app loads sinon and OPA loads it from outside. // sinon might be undefined in a module requiring it. So the workaround comes here: // trigger the load of sinon - wait until it is defined. Only when it is defined continue loading other modules loadSinon(loadFrameModules); } else { // no workaround - directly load all other modules loadFrameModules(); } } function afterModulesLoaded (oFrameLog) { // forward OPA log messages from the inner iframe to the Log listener of the outer frame // the listener should already be created and started by OPA oFrameLog.addLogListener(_LogCollector.getInstance()._oListener); bUi5Loaded = true; } function registerFrameResourcePaths () { //All Opa related resources in the iframe should be the same version //that is running in the test and not the (evtl. not available) version of Opa of the running App. registerAbsoluteResourcePathInIframe("sap/ui/test"); registerAbsoluteResourcePathInIframe("sap/ui/qunit"); registerAbsoluteResourcePathInIframe("sap/ui/thirdparty"); } /** * Disables most of the navigation in an IFrame, only setHash has an effect on the real IFrame history after running this function. * Reason: replace hash does not work in an IFrame so it may not be called at all. * This makes it necessary to hook into all navigation methods * @private */ function modifyIFrameNavigation (hasher, History, HashChanger) { var oHashChanger = new HashChanger(), oHistory = new History(oHashChanger), fnOriginalSetHash = hasher.setHash, fnOriginalGetHash = hasher.getHash, sCurrentHash, bKnownHashChange = false, fnOriginalGo = oFrameWindow.history.go; // replace hash is only allowed if it is triggered within the inner window. Even if you trigger an event from the outer test, it will not work. // Therefore we have mock the behavior of replace hash. If an application uses the dom api to change the hash window.location.hash, this workaround will fail. hasher.replaceHash = function (sHash) { bKnownHashChange = true; var sOldHash = this.getHash(); sCurrentHash = sHash; // fire the secret events for the local history so the recording is correct. // The hash changer is not the global singleton it is a local one only used in this scope for the history. oHashChanger.fireEvent("hashReplaced",{ sHash : sHash }); this.changed.dispatch(sHash, sOldHash); }; hasher.setHash = function (sHash) { bKnownHashChange = true; var sRealCurrentHash = fnOriginalGetHash.call(this); sCurrentHash = sHash; // fire the secret events for the local history so the recording is correct. // The hash changer is not the global singleton it is a local one only used in this scope for the history. oHashChanger.fireEvent("hashSet", { sHash : sHash }); fnOriginalSetHash.apply(this, arguments); // Happens when setHash("a") back setHash("a") is called. // Then dispatch the previous hash as new one because hasher does not dispatch if the real hash stays the same if (sRealCurrentHash === this.getHash()) { // always dispatch the current position of the history, since this can only happen in the backwards / forwards direction this.changed.dispatch(sRealCurrentHash, oHistory.aHistory[oHistory.iHistoryPosition]); } }; // This function also needs to be manipulated since hasher does not know about our intercepted replace hasher.getHash = function() { //initial hash if (sCurrentHash === undefined) { return fnOriginalGetHash.apply(this, arguments); } return sCurrentHash; }; // when a link is clicked or the hash is directly set we only get a changed event. hasher.changed.add(function (sNewHash) { // only if the change does not come from the other known places it is likely to be a pressed link if (!bKnownHashChange) { // fire the secret events for the local history so the recording is correct. // The hash changer is not the global singleton it is a local one only used in this scope for the history. oHashChanger.fireEvent("hashSet", { sHash : sNewHash }); sCurrentHash = sNewHash; } bKnownHashChange = false; }); oHashChanger.init(); function goBack () { bKnownHashChange = true; var sNewPreviousHash = oHistory.aHistory[oHistory.iHistoryPosition], sNewCurrentHash = oHistory.getPreviousHash(); sCurrentHash = sNewCurrentHash; hasher.changed.dispatch(sNewCurrentHash, sNewPreviousHash); } function goForward () { bKnownHashChange = true; var sNewCurrentHash = oHistory.aHistory[oHistory.iHistoryPosition + 1], sNewPreviousHash = oHistory.aHistory[oHistory.iHistoryPosition]; if (sNewCurrentHash === undefined) { Log.error("Could not navigate forwards, there is no history entry in the forwards direction", this); return; } sCurrentHash = sNewCurrentHash; hasher.changed.dispatch(sNewCurrentHash, sNewPreviousHash); } oFrameWindow.history.back = goBack; oFrameWindow.history.forward = goForward; oFrameWindow.history.go = function (iSteps) { if (iSteps === -1) { goBack(); return; } else if (iSteps === 1) { goForward(); return; } Log.error("Using history.go with a number greater than 1 is not supported by OPA5", this); return fnOriginalGo.apply(oFrameWindow.history, arguments); }; } function loadFrameModules() { oFrameWindow.sap.ui.require([ "sap/base/Log", "sap/ui/test/OpaPlugin", "sap/ui/test/autowaiter/_autoWaiter", "sap/ui/test/_OpaLogger", "sap/ui/qunit/QUnitUtils", "sap/ui/thirdparty/jquery", "sap/ui/thirdparty/hasher", "sap/ui/core/routing/History", "sap/ui/core/routing/HashChanger", "sap/ui/events/FocusEventFix" ], function ( Log, OpaPlugin, _autoWaiter, _OpaLogger, QUnitUtils, frameJQuery, hasher, History, HashChanger /* FocusEventFix */ ) { _OpaLogger.setLevel(sOpaLogLevel); oFramePlugin = new OpaPlugin(); oAutoWaiter = _autoWaiter; oFrameUtils = QUnitUtils; oFrameJQuery = frameJQuery; if (!bDisableHistoryOverride) { modifyIFrameNavigation(hasher, History, HashChanger); } FrameHashChanger = HashChanger; afterModulesLoaded(Log); }); } function getAbsolutePath(sResource) { var sResourceURL = sap.ui.require.toUrl(sResource); var oURL = new URL(sResourceURL, document.baseURI); oURL.search = oURL.hash = ""; return oURL.href; } function registerAbsoluteResourcePathInIframe(sResource) { var sAbsoluteOpaPath = getAbsolutePath(sResource); var fnConfig = ObjectPath.get("sap.ui.loader.config", oFrameWindow); if (fnConfig) { var paths = {}; paths[sResource] = sAbsoluteOpaPath; fnConfig({ paths: paths }); return; } /** * @deprecated since 1.58 as jQuery.sap.registerResourcePath has been deprecated. */ if (oFrameJQuery && oFrameJQuery.sap && oFrameJQuery.sap.registerResourcePath) { oFrameJQuery.sap.registerResourcePath(sResource, sAbsoluteOpaPath); return; } throw new Error("iFrameLauncher.js: UI5 module system not found."); } function destroyFrame () { if (!oFrameWindow) { throw new Error("sap.ui.test.launchers.iFrameLauncher: Teardown was called before launch. No iFrame was loaded."); } // Workaround for IE - there are errors even after removing the frame so setting the onerror to noop again seems to be fine oFrameWindow.onerror = jQuery.noop; for (var i = 0; i < $Frame.length; i++) { $Frame[0].src = "about:blank"; $Frame[0].contentWindow.document.write(''); $Frame[0].contentWindow.close(); } if ( typeof CollectGarbage == "function") { CollectGarbage(); // eslint-disable-line } $Frame.remove(); $FrameContainer.remove(); oFrameJQuery = null; oFramePlugin = null; oFrameUtils = null; oFrameWindow = null; bUi5Loaded = false; bRegisteredToUI5Init = false; oAutoWaiter = null; FrameHashChanger = null; } /** * Contains the logic to place an iFrame to the dom and overwrites the navigation inside of it. * Every launch call needs a corresponding teardown call, or errors will be logged and the calls will be ignored. * Please have a look at {@link sap.ui.test.Opa5#iStartMyAppInAFrame} and {@link sap.ui.test.Opa5#iTeardownMyAppFrame}for the public functions using this class. * @private */ return { launch: function (options) { if (oFrameWindow) { throw new Error("sap.ui.test.launchers.iFrameLauncher: Launch was called twice without teardown. Only one iFrame may be loaded at a time."); } //invalidate the cache $Frame = jQuery("#" + options.frameId); if ($Frame.length) { $FrameContainer = jQuery(".opaFrameContainer"); } else { if (!options.source) { Log.error("No source was given to launch the IFrame", this); } //invalidate other caches $FrameContainer = jQuery("<div class='opaFrameContainer'></div>"); $Frame = jQuery('<IFrame id="' + options.frameId + '" class="opaFrame" src="' + options.source + '"></IFrame>'); $FrameContainer.append($Frame); jQuery("body").append($FrameContainer); setFrameSize(options.width, options.height); } if ($Frame[0].contentDocument && $Frame[0].contentDocument.readyState === "complete") { handleFrameLoad(); } else { $Frame.on("load", handleFrameLoad); } sOpaLogLevel = options.opaLogLevel; bDisableHistoryOverride = options.disableHistoryOverride; return checkForUI5ScriptLoaded(); }, hasLaunched: function () { checkForUI5ScriptLoaded(); return bUi5Loaded; }, teardown: function () { destroyFrame(); }, getHashChanger: function () { if (!FrameHashChanger) { return null; } return FrameHashChanger.getInstance(); }, getPlugin: function () { return oFramePlugin; }, getJQuery: function () { return oFrameJQuery; }, getUtils: function () { return oFrameUtils; }, getWindow: function () { return oFrameWindow; }, _getAutoWaiter: function () { return oAutoWaiter; } }; }, /* export= */ true);