Skip to Content
Technical Articles

Fiori Elements and Testing with SAP Web IDE: Implementing the Changes Files Through Testing

(This blog post assumes your project is already setup with Fiori Elements, QUnit and OPA5, and that your project has changes (or files with extension “.change”). You might want to know how to code asynchronous JavaScript to understand what I did.)

Edit: I realize that not everyone has the testsuite.qunit.html file. In case you do not, imagine that file as being the target file where changes need to be accessible from.

Working with Fiori Elements, I have recently encountered an issue using the regular template for my testsuite.qunit.html file. For some unknown reason, my opaTests.qunit.html file would receive the changes applied in my project (through the changes folder), but testsuite.qunit.html would not.

This led me to a month long journey to find an answer to this problem. A colleague of mine had found an answer, but that answer had deprecated methods which I could no longer use. So, I had to find one myself.

The process of loading these changes is hidden in SAP Web IDE. Some files are hidden even if the user presses the “Show Hidden Files” icon which is represented as an eye. However, I found out a way to access it.

The file that is loading the changes is called changes_preview.js. You can get that file by clicking on the GIT symbol on the sidebar on the right of the screen in SAP Web IDE. There, you will most likely see the changes_preview.js file. You can see this file because it gets updated every now and then.

Inside that changes_preview.js file, you will see how SAP Web IDE loads changes to your application. Double-click on the name of that file to see its contents.

If this does not work, you can always export the project and search for this file. You can find it directly in the main directory of your project ( exported_project_dir/ changes_preview.js). I have attached a version of the file below.

If you only want to know the solution, take directly a look into the The Solution section.

Issue #1: Testsuite does not directly import the “changes_preview” file

When testing a Fiori Elements application and running the tests on testsuite.qunit.html, the changes are not being loaded. However, they are getting loaded into the opaTests.qunit.html file. The reason is simple: those changes are directly loaded in the opaTests.qunit.html file, but not in the testsuite.qunit.html file.

This is the code present in opaTests.qunit.html that loads the changes:

<script src="../../../changes_preview.js"></script>
<script>(function(){
	     document.addEventListener("load",function(e){
                 if("OpaFrame"===e.target.id){
	
                    var t=document.getElementById("OpaFrame").contentWindow.document,
                        n=t.createElement("script");
                    n.type="text/javascript",n.innerHTML="sap.ui.getCore().loadLibrary('sap.ui.fl'); 
                    jQuery.getScript('../../../changes_preview.js')",t.head.appendChild(n)
                 }
             },!0)
         })()
</script>

As a temporary solution, you could add this code in your testsuite.qunit.html above the line that includes the script qunit-redirect.js. I say temporary, because if you are experiencing the issues I am describing below, this solution will not work.

 

Issue #2: Jenkins changes the project’s structure

You read right: Jenkins messes with your project’s structure. Indeed, Jenkins removes the webapp folder and renames your test folder to test-resources. In order to resolve that issue, I had to copy my changes folder into the test folder. This way, the changes would be in the same directory as my test files.

However, my opaTests.qunit.html file is not in the same directory as my testsuite.qunit.html. This means I would have to recopy a third time the changes folder, which I do not want. It is easier, then, to simply modify the file handling your mock server initialization (in my case, flpSandboxMockServer.html).

Note: Repo is my webapp folder.

Issue #3: SAPUI5 manipulates an Ajax call

After I had copied and adjusted the code in the changes_preview.js file for the flpSandboxMockServer.html file, I realized that Jenkins would still not accept my code. The reason? Tomcat does not support the way an Ajax call was made. This, however, did not originate from the code that I copied; something inside the library was using the Ajax call to change the original request.

Here is the original changes_preview.js file:

//This file used only for loading the changes in the webide preview and not required to be checked in.
//Load the fake lrep connector
sap.ui.require(["sap/ui/fl/FakeLrepConnector"], function (FakeLrepConnector) {
	jQuery.extend(FakeLrepConnector.prototype, {
		create: function (oChange) {
			return Promise.resolve();
		},
		stringToAscii: function (sCodeAsString) {
			if (!sCodeAsString || sCodeAsString.length === 0) {
				return "";
			}
			var sAsciiString = "";
			for (var i = 0; i < sCodeAsString.length; i++) {
				sAsciiString += sCodeAsString.charCodeAt(i) + ",";
			}
			if (sAsciiString !== null && sAsciiString.length > 0 && sAsciiString.charAt(sAsciiString.length - 1) === ",") {
				sAsciiString = sAsciiString.substring(0, sAsciiString.length - 1);
			}
			return sAsciiString;
		},
		/*
		 * Get the content of the sap-ui-cachebuster-info.json file
		 * to get the paths to the changes files
		 * and get their content
		 */
		loadChanges: function () {
			var oResult = {
				"changes": [],
				"settings": {
					"isKeyUser": true,
					"isAtoAvailable": false,
					"isProductiveSystem": false
				}
			};

			//Get the content of the changes folder.
			var aPromises = [];
			var sCacheBusterFilePath = "/sap-ui-cachebuster-info.json";

			/*eslint-disable promise/avoid-new*/
			/*eslint-disable promise/catch-or-return*/
			/*eslint-disable promise/always-return*/
			/*eslint-disable promise/no-nesting*/
			/*eslint-disable consistent-return*/
			/*eslint-disable xss/no-mixed-html*/
			return new Promise(function (resolve, reject) {
				$.ajax({
					url: window.location.origin + sCacheBusterFilePath,
					type: "GET",
					cache: false
				}).then(function (oCachebusterContent) {
					//we are looking for only change files
					var aChangeFilesPaths = Object.keys(oCachebusterContent).filter(function (sPath) {
						return sPath.endsWith(".change");
					});
					$.each(aChangeFilesPaths, function (index, sFilePath) {
						//now as we support MTA projects we need to take only changes which are relevant for 
						//the current HTML5 module
						//sap-ui-cachebuster-info.json for MTA doesn't start with "webapp/changes" but from <MTA-HTML5-MODULE-NAME>
						//possible change file path patterns
						//webapp/changes/<change-file>
						//<MTA-HTML5-MODULE-NAME>/webapp/changes/<change-file>
						var sChangesRelativePathIndex = sFilePath.indexOf("webapp/changes");
						if (sChangesRelativePathIndex > 0 && sFilePath.split("/")[0] !== "your.project.name.on.git") {
							return true;
						} else {
							sFilePath = sFilePath.slice(sChangesRelativePathIndex);
							/*eslint-disable no-param-reassign*/
							aPromises.push(
								$.ajax({
									url: window.location.origin + "/" + sFilePath,
									type: "GET",
									cache: false
								}).then(function (sChangeContent) {
									return JSON.parse(sChangeContent);
								})
							);
						}
					});
				}).always(function () {
					return Promise.all(aPromises).then(function (aChanges) {
						var aChangePromises = [],
							aProcessedChanges = [];
						aChanges.forEach(function (oChange) {
							var sChangeType = oChange.changeType;
							if (sChangeType === "addXML" || sChangeType === "codeExt") {
								/*eslint-disable no-nested-ternary*/
								var sPath = sChangeType === "addXML" ? oChange.content.fragmentPath : sChangeType === "codeExt" ? oChange.content.codeRef :
									"";
								var sWebappPath = sPath.match(/webapp(.*)/);
								var sUrl = "/" + sWebappPath[0];
								aChangePromises.push(
									$.ajax({
										url: sUrl,
										type: "GET",
										cache: false
									}).then(function (oFileDocument) {
										if (sChangeType === "addXML") {
											oChange.content.fragment = FakeLrepConnector.prototype.stringToAscii(oFileDocument.documentElement.outerHTML);
											oChange.content.selectedFragmentContent = oFileDocument.documentElement.outerHTML;
										} else if (sChangeType === "codeExt") {
											oChange.content.code = FakeLrepConnector.prototype.stringToAscii(oFileDocument);
											oChange.content.extensionControllerContent = oFileDocument;
										}
										return oChange;
									})
								);
							} else {
								aProcessedChanges.push(oChange);
							}
						});
						// aChanges holds the content of all change files from the project (empty array if no such files)
						// sort the array by the creation timestamp of the changes
						if (aChangePromises.length > 0) {
							return Promise.all(aChangePromises).then(function (aUpdatedChanges) {
								aUpdatedChanges.forEach(function (oChange) {
									aProcessedChanges.push(oChange);
								});
								aProcessedChanges.sort(function (change1, change2) {
									return new Date(change1.creation) - new Date(change2.creation);
								});
								oResult.changes = aProcessedChanges;
								var oLrepChange = {
									changes: oResult,
									componentClassName: "your.project.name.on.git"
								};
								resolve(oLrepChange);
							});
						} else {
							aProcessedChanges.sort(function (change1, change2) {
								return new Date(change1.creation) - new Date(change2.creation);
							});
							oResult.changes = aProcessedChanges;
							var oLrepChange = {
								changes: oResult,
								componentClassName: "your.project.name.on.git"
							};
							resolve(oLrepChange);
						}
					});
				});
			});
		}
	});
	FakeLrepConnector.enableFakeConnector();
});

Essentially, this file is getting the content of the SAP Web IDE sap-ui-cachebuster-info.json file and gets the paths to the changes files. Once this is done, an Ajax call is created for each one of those changes files to get their content. Then, this content is stored into the variable oLrepChange, which is then returned by the method loadChanges.

There is also a verification done to see whether the change is of type addXml or codeExt, and if so, some modifications are applied to those changes. This solution, however, does not include that part of code.

The first Ajax call, using the URL window.location.origin + sCacheBusterFilePath, is the Ajax call that generates issues. SAPUI5 will start using that Ajax call to add parameters in the form of ?_=155309439922 or something similar (indicates a query for a specific time in milliseconds), which Tomcat does not support.

This is the reason why this Ajax call needs to be removed. Changing the name of the file or even changing the content of the file did not remove the query to the URL.

 

The Solution

This solution includes a solution to the problem of paths and to the problem with the Ajax call. The only thing that I touched from the changes_preview.js file is the loadChanges method.

I added some context around the solution so that you can identify where this goes (separated by the —8<— line). For me, this code went into the flpSandboxMockServer.html file. Notice the “your.project.name.on.git”, which is the name of the project. This is the name of your git repository.

Step by step solution:

1- Copy your changes folder into the same directory as your flpSandboxMockServer.html file

This is because of the path issue described in Issue #2.

2- Copy this code below into your flpSandboxMockServer.html file

<script type="text/javascript">
		sap.ui.getCore().attachInit(function() {
//----------------8<----------------------------------------------------------------------------------
sap.ui.require(["sap/ui/fl/FakeLrepConnector", "your/project/name/on/git/localService/mockserver"], function (FakeLrepConnector, server) {
					jQuery.extend(FakeLrepConnector.prototype, {
						create: function (oChange) {
							return Promise.resolve();
						},
						stringToAscii: function (sCodeAsString) {
							if (!sCodeAsString || sCodeAsString.length === 0) {
								return "";
							}
							var sAsciiString = "";
							for (var i = 0; i < sCodeAsString.length; i++) {
								sAsciiString += sCodeAsString.charCodeAt(i) + ",";
							}
							if (sAsciiString !== null && sAsciiString.length > 0 && sAsciiString.charAt(sAsciiString.length - 1) === ",") {
								sAsciiString = sAsciiString.substring(0, sAsciiString.length - 1);
							}
							return sAsciiString;
						},
						/*
						 * Get the content of the sap-ui-cachebuster-info.json file
						 * to get the paths to the changes files
						 * and get their content
						 */
						loadChanges: function () {

							var oResult = {
								"changes": [],
								"settings": {
									"isKeyUser": true,
									"isAtoAvailable": false,
									"isProductiveSystem": false
								}
							};

							//Get the current path, most efficient way:
							var fullPath = window.location.href;

							//Remove the file to get the current directory
							//this is because Jenkins removes the webapp file, and so
							//I had to find a way around it: I copied the changes folder
							//in the same directory as this file, and took the path to
							//this folder by taking window.location.href, bypassing a 
							//hardcoded "webapp" folder in my path.
							var divByFolders = fullPath.split("/");
							var extractedString = divByFolders[divByFolders.length - 1];
							//Get the current directory we are in
							fullPath = fullPath.replace(extractedString, '');

							//Dont forget the trailing / at the end of the path
							//console.log("Full path: "+fullPath);

							//Get the content of the changes folder.
							var aPromises = [];

							//copied this from sap-ui-cachebuster-info.json. You can access
							//that file in SAP Web IDE by creating a new file and naming it
							//"sap-ui-cachebuster-info.json", and all the content of that file
							//will appear.

							//otherwise do them one by one. NOTE: the reason I start from
							//the changes directory is because I copied the changes into the
							//same folder as this file.
							var paths = {

								"changes/a.change": 1552336604000,
								"changes/b.change": 1552336602000,
								

							};

							return new Promise(function (resolve, reject) {

									var aChangeFilesPaths = Object.keys(paths).filter(function (sPath) {
										return sPath.endsWith(".change");
									});
									//creating a counter to know how many calls were COMPLETED
									var completedCalls = 0;

									//my function to callback
									function callback(){
										//sorts the changes based on their date of creation.
										aPromises.sort(function(change1, change2){
											return new Date(change1.creation) - new Date(change2.creation);
										});

										oResult.changes = aPromises;
										var oLrepChange = {
											changes: oResult,
											componentClassName: "your.project.name.on.git"
										};
										resolve(oLrepChange);
									}

									$.each(aChangeFilesPaths, function (index, sFilePath) {

										//this pushes into aPromises new information once the ajax call has been resolved.
										$.ajax({
											url: fullPath + sFilePath,
											headers: {
									        'Accept': 'application/json',
									        'Content-Type': 'application/json'
									    },
											'dataType': 'json',
											type: "GET",
											cache: false,
										}).then(function (sChangeContent) {

											//console.log(sChangeContent);
											aPromises.push(JSON.parse(JSON.stringify(sChangeContent)));
											completedCalls+=1;
											//once all AJAX calls are completed, call the callback.
											//this number will have to be modified depending on
											//the number of changes that are in the project.
											if (completedCalls === Object.keys(paths).length){
												callback();
											}
										})


									});

							})
						}
					});

					FakeLrepConnector.enableFakeConnector();

					//initialize the server. put it in same sap.ui.require as the FakeLrep object.
					server.init();

					// initialize the ushell sandbox component
					sap.ushell.Container.createRenderer().placeAt("content");
				});
//-------------------------8<--------------------------------------------------------------------------
		});
	</script>

Issue #1 is fixed with this code, because it is put directly into the file that launches the mock server. opaTests.qunit.html and testsuite.qunit.html both must refer to this file to get the IFrame loaded for OPA.

To remedy to the issue of the paths, which is Issue #2, I took the current path using window.location.href, which I do not care about the result. I would remove the file mentioned in that path to leave only the path to the current folder, and not the path to the current file.I am then using that path to go to my changes folder.

Finally, Issue #3 is resolved because I removed the Ajax call that was getting the sap-ui-cachebuster-info.json file. Instead, I put the content of that file into the variable paths.

Notice that this code does not touch addXml or codeExt types of changes like changes_preview.js does. It does not handle those changes.

What I did different in this code was to create a callback method from the $.each method. This callback method is named callback for obvious reasons. The callback method contains the last operations that changes_preview.js was doing, which was putting the collected information from all the changes files into one single object before returning a response for the loadChanges method.

Also, I would like to add that this code above includes the mock server initialization, so you can safely remove this code if you have it (but definitely remove the first two lines of this code sample in your code, as the whole purpose of this blog is to modify the behavior of the FakeLrepConnector being generated):

jQuery.sap.require("sap.ui.fl.FakeLrepConnector");
sap.ui.fl.FakeLrepConnector.enableFakeConnector("fakeLRep.json");
sap.ui.require([
	"your/git/repo/localService/mockserver"
], function (server) {
	// set up test service for local testing
	server.init();

	// initialize the ushell sandbox component
	sap.ushell.Container.createRenderer().placeAt("content");
});

Note: This code just above is normally found inside the same file that I was modifying just above (flpSandboxMockServer.html for me), below the sap.ui.getCore().attachInit() function.

3- Set the paths variable correctly

You might wonder how to set the variable paths. You can probably get that all setup by going to the sap-ui-cachebuster-info.json file in SAP Web IDE. To do so, create a new file with the exact same name (sap-ui-cachebuster-info.json), and you will get the content of that file. Note that you will have to change the paths to start with the folder changes, not webapp. Remember that the changes folder was copied into the same directory as that of flpSandboxMockServer.html, which is why we can set the path to start with the folder changes.

If that does not work, you can always set it yourself; I believe the numbers are not really important, because all the code is doing is extracting the paths to those files to create Ajax calls based on those URLs. The sorting of the files is not even done with that number.

4- Remove the code that searches for the changes_preview.js file

In Issue #1, I presented some code snippet that allowed the program to execute the changes_preview.js script. You have to remove that code from the opaTests.qunit.html file, and maybe the testsuite.qunit.html file if you have added that code. This way, there will be no confusion as to where you loaded your changes from. I have not tested my application with that code, so I do not know if it will only add 404 errors or if it will crash the application under Tomcat.

 

Edit: Remove the changes_preview.js file

This step might or might not be necessary. It depends on whether you receive the requests that are in the image below.

I have noticed that the file was causing me issues so I removed it. To do so, you can try to get the changes_preview file content like I did at the beginning of the article. From there, you can delete the file. If that does not work, you need to export your project and import it back without the changes_preview file.

The reason to remove the changes_preview file is because, since the modifications that were brought to the application in this tutorial, the code of that file releases requests that the application is not able to handle and will thus crash the application that launches through flpSandbox.,

Warning: For some reason, the changes_preview file, even if it is deleted, will come back eventually. It might not be possible to see it like I do (in the first part of this tutorial), so you will need to export your project, remove the file, and import back your project into the Web IDE. It is possible to know if the changes_preview file has been deleted by looking into your web devtools (to open, right click on the page, and select inspect element). If the application flpSandbox generates these requests, then you know the file has not been deleted:

These requests are generated because the Web IDE is trying to access some changes in your project by some timestamp (hence, the number in the request). It looks into the sap-ui-cachebuster-info.json file and tries to get the files, but for some reason fails. It might be because I have made a copy of the changes…

Edit: Add some code in flpSandbox file

I do not know if this step is mandatory; I have heard of people that did not do it. However, if the flpSandbox file is not working correctly, you should follow this procedure described below.

The code that needs to be added is the same code as shown in step 2 of the solution. The only thing you need to modify is to comment out the “server.init()” command (second to last instruction), and you can comment out the last dependency in the sap.ui.require array, as well as remove the second parameter of the function. The code goes in the same place as in the flpSandboxMockServer file.

You will need to comment out the line sap.ushell.Container.createRenderer().placeAt(“Content”) because that line is already included in my code snippet from step 2.

 

I hope this blog post will encourage some people to use Fiori Elements. I certainly would have been more cheerful to use Fiori Elements if there had been a post like this before, as I feel it is already a difficult tool to use as it is. I wonder if a feature is going to be included in SAP Web IDE to support this issue in the near future.

-Alexandre Therrien

2 Comments
You must be Logged on to comment or reply to a post.