Skip to Content
Author's profile photo Laszlo Kajan

Headless OPA5 Testing of Fiori Launchpad Apps with Code Coverage and Karma

Executive summary

OPA5 integration testing of SAP Fiori launchpad apps, including code coverage, isn’t supported by sap.ui.test.Opa5. This blog post provides a solution.

Automation of OPA5 integration testing of SAP Fiori launchpad apps with code coverage is also presented.

Author and motivation

Laszlo Kajan is a full stack Fiori/SAPUI5 expert, present on the SAPUI5 field since 2015.

The motivation behind this blog post is to provide an automated integration testing solution for Fiori launchpad apps – a feature so far missing from sap.ui.test.Opa5.

Headless OPA5 testing of Fiori launchpad apps with code coverage and Karma

Goal

  • Implement automated OPA5 integration tests for SAPUI5 launchpad apps
  • Provide code coverage results
  • Employ current OPA5 best practices

App folder structure

  • webapp
    • test
      • integration
        • arrangement
          • component
            • Arrangement.js
        • pages
          • Common.js
        • AllJourneys.js
        • opaTestsWithComponent.qunit.html
      • karma
        • context.html
      • launchers
        • ushellLauncher.js
    • fakeLRep.json
    • test.html
  • karma.conf.js
  • package.json

Issue 1: Missing ‘iStartMyUIComponentInUshell’

The SAP WebIDE runs Fiori launchpad apps with mock data by running the app’s Component.js in a sap.ushell.Container. OPA5 testing allows apps to be started in an iframe – iStartMyAppInAFrame, or by their Component.js – iStartMyUIComponent. As of 20180809, test code coverage analysis only works in case the app is not started in an iframe. When iStartMyUIComponent is used to start the app, unlike starting it with mock data, Component.js is not placed into a sap.ushell.Container. Because of this, application code that otherwise works well – e.g. access to launchpad services – fails, leading to test errors. This is the first issue to solve.

How to run OPA5 integration tests with code coverage results, in a way similar to how the app is run with mock data? There is no OPA5 method ‘iStartMyUIComponentInUshell’ that would start a Component.js in a sap.ushell.Container. Let us define one:

  1. webapp/test/integration/arrangement/component/Arrangement.js:
    // 20180808 openui5/src/sap.m/test/sap/m/demokit/cart
    // https://github.com/SAP/openui5/blob/master/src/sap.m/test/sap/m/demokit/cart/webapp/test/integration/arrangement/component/Arrangement.js
    sap.ui.define([
    	"sap/ui/test/Opa5",
    	"sap/ui/core/routing/HashChanger",
    	"com/acme/top/wdsp/mon/test/launchers/ushellLauncher"
    ], function(Opa5, HashChanger, launcher) {
    	"use strict";
    
    	// Copy from /sap/ui/test/Opa5-dbg.js
    	function createWaitForObjectWithoutDefaults() {
    		return {
    			// make sure no controls are searched by the defaults
    			viewName: null,
    			controlType: null,
    			id: null,
    			searchOpenDialogs: false,
    			autoWait: false
    		};
    	}
    
    	var Arrangement = Opa5.extend("com.acme.top.wdsp.mon.test.integration.arrangement.component.Arrangement", {
    
    		iStartMyUIComponentInUshell: function(oOptions) {
    			var bComponentLoaded = false;
    			oOptions = oOptions || {};
    
    			var oFirstWaitForOptions = createWaitForObjectWithoutDefaults();
    			oFirstWaitForOptions.success = function() {
    				// include stylesheet
    				var sComponentStyleLocation = jQuery.sap.getModulePath("sap.ui.test.OpaCss", ".css");
    				$.sap.includeStyleSheet(sComponentStyleLocation);
    
    				if (oOptions.hash) {
    					HashChanger.getInstance().setHash(oOptions.hash);
    				}
    
    				launcher.start().then(function() {
    					bComponentLoaded = true;
    				});
    			};
    			// wait for starting of component launcher
    			this.waitFor(oFirstWaitForOptions);
    
    			var oPropertiesForWaitFor = createWaitForObjectWithoutDefaults();
    			oPropertiesForWaitFor.errorMessage = "Unable to start launcher with hash: " + oOptions.hash;
    			oPropertiesForWaitFor.check = function() {
    				return bComponentLoaded;
    			};
    
    			// add timeout to object for waitFor when timeout is specified
    			if (oOptions.timeout) {
    				oPropertiesForWaitFor.timeout = oOptions.timeout;
    			}
    
    			return this.waitFor(oPropertiesForWaitFor);
    		},
    
    		iStartTheApp: function(oOptions) {
    			oOptions = oOptions || {};
    			return this.iStartMyUIComponentInUshell({
    				hash: oOptions.hash
    			});
    		}
    	});
    	return Arrangement;
    });​
  2. webapp/test/launchers/ushellLauncher.js:
    // Based on https://sapui5.hana.ondemand.com/1.44.38/resources/sap/ui/test/launchers/componentLauncher-dbg.js
    /*!
     * UI development toolkit for HTML5 (OpenUI5)
     * (c) Copyright 2009-2016 SAP SE or an SAP affiliate company.
     * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
     */
    sap.ui.define([
    	'jquery.sap.global'
    ], function(jQuery) {
    	"use strict";
    	var $ = jQuery,
    		_loadingStarted = false,
    		_oComponentContainer = null,
    		_$Component = null;
    
    	/**
    	 * By using start launcher will instantiate and place the sap.ushell.Container into html.
    	 * By using teardown launcher will destroy the sap.ushell.Container and remove the div from html.
    	 * Calling start twice without teardown is not allowed
    	 * @private
    	 * @class
    	 * @author SAP SE
    	 * @alias sap.ui.test.launchers.ushellLauncher
    	 */
    	return {
    
    		start: function() {
    			if (_loadingStarted) {
    				throw "sap.ui.test.launchers.componentLauncher: Start was called twice without teardown";
    			}
    
    			var oPromise = Promise.resolve();
    
    			_loadingStarted = true;
    
    			return oPromise.then(function() {
    				var sId = jQuery.sap.uid();
    
    				// create and add div to html
    				_$Component = $('<div id="' + sId + '" class="sapUiOpaComponent"></div>');
    				/* eslint-disable sap-no-dom-insertion */
    				$("body").append(_$Component).addClass("sapUiOpaBodyComponent");
    				/* eslint-disable sap-no-dom-insertion */
    
    				// create and place the component into html
    				_oComponentContainer = sap.ushell.Container.createRenderer();
    
    				_oComponentContainer.placeAt(sId);
    			});
    
    		},
    
    		hasLaunched: function() {
    			return _loadingStarted;
    		},
    
    		teardown: function() {
    			// Opa prevent the case if teardown was called after the start but before the promise was fulfilled
    			if (!_loadingStarted) {
    				throw "sap.ui.test.launchers.componentLauncher: Teardown has been called but there was no start";
    			}
    			_oComponentContainer.destroy();
    			_$Component.remove();
    			_loadingStarted = false;
    			$("body").removeClass("sapUiOpaBodyComponent");
    		}
    	};
    
    }, /* export= */ true);​

Run the test like this:

  1. webapp/test.html:
    <!DOCTYPE html>
    <html>
    	<head>
    		<title>Testing Overview</title>
    		<!--  try to load the basic UI5 styles -->
    		<link rel="stylesheet" type="text/css" href="resources/sap/ui/core/themes/sap_bluecrystal/library.css">
    	</head>
    	<body class="sapUiBody sapUiMediumMargin sapUiForceWidthAuto">
    		<h1>Testing Overview</h1>
    		<p>This is an overview page of various ways to test the generated app during development.<br/>Choose one of the access points below to launch the app as a standalone application, e.g. on a Tomcat server.</p>
    
    		<ul>
    			<li><a href="test/integration/opaTestsWithComponent.qunit.html?coverage">test/integration/opaTestsWithComponent.qunit.html</a> - run all integration tests with Component</li>
    		</ul>
    	</body>
    </html>​
  2. webapp/test/integration/opaTestsWithComponent.qunit.html:
    <!DOCTYPE html>
    <html style="overflow:auto">
    	<head>
    		<title>Integration tests for com.acme.top.wdsp.mon with Component</title>
    		<meta http-equiv='X-UA-Compatible' content='IE=edge'>
    		<meta charset="utf-8">
    		
    		<script type="text/javascript">
    			window["sap-ushell-config"] = {
    				defaultRenderer : "fiori2",
    				renderers: {
    					fiori2: {
    						componentData: {
    							config: {
    								rootIntent: "ZF67_WDAYSP-monitor",
    								search: "hidden"
    							}
    						}
    					}
    				},
    				applications: {
    					"ZF67_WDAYSP-monitor": {
    						"additionalInformation": "SAPUI5.Component=com.acme.top.wdsp.mon",
    						"applicationType": "URL",
    						"url": "../../"
    					}
    				}
    			};
    		</script>
    		<script src="https://sapui5.hana.ondemand.com/test-resources/sap/ushell/bootstrap/sandbox.js" id="sap-ushell-bootstrap"></script>
    
    		<!--
    		src="https://sapui5.hana.ondemand.com/1.44.38/resources/sap-ui-core.js"
    		src="../../resources/sap-ui-core.js"
    		-->
    
    		<!-- The namespace sap.ui.demo.cart.test.arrangement.Arrangement is used to run the IFrame or the component without changes to the test code -->
    		<!--script>
    			// Note 20180809: loading sap-ui-core this way causes all sorts of problems with sap.ushell.Container loading,
    			//	regardless of the "preload" setting. It doesn't work, must use the data-sap-ui... solution below.
    			window["sap-ui-config"] = {
    				"animation":"false",
    				"compatVersion":"edge",
    				"debug":"false",
    				"frameOptions":"deny",
    				"language":"en",
    				"theme":"sap_belize",
    				"libs":"sap.m, sap.ushell",
    				"resourceRoots":{
    					"com.acme.top.wdsp.mon":"../../",
    					"Arrangement":"./arrangement/component/Arrangement"},
    				"preload":"async",
    				"xx-debugModuleLoading":"true",
    				"xx-showLoadErrors":"true",
    				"xx-supportedLanguages":["en"]};
    		</script>
    		<script id="sap-ui-bootstrap"
    				src="../../resources/sap-ui-core.js">
    		</script-->
    		<script id="sap-ui-bootstrap"
    				src="../../resources/sap-ui-core.js"
    				data-sap-ui-animation="false"
    				data-sap-ui-compatVersion="edge"
    				data-sap-ui-frameOptions='deny'
    				data-sap-ui-language="en"
    				data-sap-ui-libs='sap.m, sap.ushell'
    				data-sap-ui-preload='async'
    				data-sap-ui-resourceroots='{
    					"com.acme.top.wdsp.mon" : "../../",
    					"Arrangement": "./arrangement/component/Arrangement"
    				}'
    				data-sap-ui-theme="sap_belize"
    				data-sap-ui-xx-supportedLanguages="">
    		</script>
    
    		<script src="../../resources/sap/ui/qunit/qunit-css.js"></script>
    		<script src="../../resources/sap/ui/thirdparty/qunit.js"></script>
    		<script src="../../resources/sap/ui/qunit/qunit-junit.js"></script>
    		<script src="../../resources/sap/ui/qunit/qunit-coverage.js"
    			data-sap-ui-cover-only="com/acme/top/wdsp/mon/"
    			data-sap-ui-cover-never="[com/acme/top/wdsp/mon/localService/, com/acme/top/wdsp/mon/test/]">
    		</script>
    
    		<script>
    			// https://github.com/SAP/openui5/blob/master/src/sap.m/test/sap/m/demokit/cart/webapp/test/integration/opaTestsWithComponent.qunit.html
    			// we want to be able to load our tests asynchronously - pause QUnit until we loaded everything
    			QUnit.config.autostart = false;
    			
    			sap.ui.getCore().attachInit(function () {
    				"use strict";
    				sap.ui.require([
    					"sap/ui/fl/FakeLrepConnector",
    					"com/acme/top/wdsp/mon/localService/mockserver",
    					"com/acme/top/wdsp/mon/test/integration/AllJourneys"
    				], function (FakeLrepConnector, server) {
    					//Fake LREP
    					FakeLrepConnector.enableFakeConnector("../../fakeLRep.json");
    				
    					// set up test service for local testing
    					server.init({autoRespondAfter: 50});
    
    					// configuration has been applied and the tests in the journeys have been loaded - start QUnit
    					QUnit.start();
    				});
    			});
    		</script>
    	</head>
    	<body>
    		<div id="qunit"></div>
    		<div id="qunit-fixture"></div>
    	</body>
    </html>​

     

  3. webapp/fakeLRep.json:
    {
    	"changes": [],
    	"settings": {
    		"isKeyUser": true,
    		"isAtoAvailable": false,
    		"isProductiveSystem": false
    	}
    }​
  4. webapp/test/integration/AllJourneys.js:
    // https://github.com/SAP/openui5/blob/master/src/sap.m/test/sap/m/demokit/cart/webapp/test/integration/AllJourneys.js
    sap.ui.define([
    	"sap/ui/test/Opa5",
    	"Arrangement",
    	"./ListJourney",
    	//"./BusyJourney"
    ], function(Opa5, Arrangement) {
    	"use strict";
    
    	Opa5.extendConfig({
    		arrangements: new Arrangement(),
    		viewNamespace: "com.acme.top.wdsp.mon.view.",
    		autoWait: true
    	});
    });​
  5. webapp/test/integration/ListJourney.js:
    /* global QUnit */
    sap.ui.define([
    		"sap/ui/test/opaQunit",
    		"./pages/List"
    	], function (opaTest) {
    		"use strict";
    
    		QUnit.module("List Page Journey");
    
    		opaTest("Should see the list with all entries", function (Given, When, Then) {
    			// Arrangements
    			Given.iStartTheApp();
    
    			//Actions
    			When.onTheListPage.iLookAtTheScreen();
    
    			// Assertions
    			Then.onTheListPage.iShouldSeeTheList().
    				and.theListShouldHaveAllEntries().
    				and.theHeaderShouldDisplayAllEntries();
    				
    			Then.onTheListPage.iTeardownMyApp();
    		});
    	}
    );​
  6. webapp/test/integration/pages/List.js:
    sap.ui.define([
    		"sap/ui/test/Opa5",
    		"sap/ui/test/actions/Press",
    		"sap/ui/test/actions/EnterText",
    		"sap/ui/test/matchers/AggregationLengthEquals",
    		"sap/ui/test/matchers/AggregationFilled",
    		"sap/ui/test/matchers/PropertyStrictEquals",
    		"com/acme/top/wdsp/mon/test/integration/pages/Common"
    	], function(Opa5, Press, EnterText, AggregationLengthEquals, AggregationFilled, PropertyStrictEquals, Common) {
    		"use strict";
    
    		var sViewName = "List",
    			sSomethingThatCannotBeFound = "*#-Q@@||",
    			iGroupingBoundary = 100;
    
    		Opa5.createPageObjects({
    			onTheListPage : {
    				baseClass : Common,
    
    				actions : {
    				},
    
    				assertions : {
    
    					iShouldSeeTheList : function () {
    						return this.waitFor({
    							id : "innerUi5Table",
    							viewName : sViewName,
    							success : function (oList) {
    								Opa5.assert.ok(oList, "Found the list");
    							},
    							errorMessage : "Can't see the list."
    						});
    					},
    
    					theListShouldHaveAllEntries : function () {
    						var aAllEntities,
    							iExpectedNumberOfItems;
    						// retrieve all TransferSet to be able to check for the total amount
    						return this.waitFor(this.createAWaitForAnEntitySet({
    							entitySet : "ZF67_C_HRMON",
    							success : function (aEntityData) {
    								aAllEntities = aEntityData;
    								return this.waitFor({
    									id : "innerUi5Table",
    									viewName : sViewName,
    									matchers : function (oList) {
    										// If there are less items in the table than the growingThreshold, only check for this number.
    										iExpectedNumberOfItems = Math.min(oList.getGrowingThreshold(), aAllEntities.length);
    										return new AggregationLengthEquals({name : "items", length : iExpectedNumberOfItems}).isMatching(oList);
    									},
    									success : function (oList) {
    										Opa5.assert.strictEqual(oList.getItems().length, iExpectedNumberOfItems, "The table displays all items");
    									},
    									errorMessage : "Table does not display all entries."
    								});
    							}
    						}));
    					},
    
    					theHeaderShouldDisplayAllEntries : function () {
    						return this.waitFor({
    							id : "innerUi5Table",
    							viewName : sViewName,
    							success : function (oList) {
    								var iExpectedLength = oList.getBinding("items").getLength();
    								this.waitFor({
    									id : "smartTable",
    									viewName : sViewName,
    									success : function (oSmartTable) {
    										var sTableHeader = oSmartTable.getDomRef().querySelector("div > div > div > span").textContent;
    										Opa5.assert.strictEqual(sTableHeader, "Transfers (11)", "The header should show 'Transfers (11)'");
    									},
    									errorMessage : "The table 'smartTable' was not found"
    								});
    							},
    							errorMessage : "Table title does not display the number of items in the list"
    						});
    					},
    				}
    			}
    		});
    	}
    );​
  7. webapp/test/integration/pages/Common.js:
    sap.ui.define([
    	"sap/ui/test/Opa5",
    	"com/acme/top/wdsp/mon/test/launchers/ushellLauncher"
    ], function(Opa5, launcher) {
    	"use strict";
    
    	// Copy from /sap/ui/test/Opa5-dbg.js
    	function createWaitForObjectWithoutDefaults() {
    		return {
    			// make sure no controls are searched by the defaults
    			viewName: null,
    			controlType: null,
    			id: null,
    			searchOpenDialogs: false,
    			autoWait: false
    		};
    	}
    
    	return Opa5.extend("com.acme.top.wdsp.mon.test.integration.pages.Common", {
    
    		iLookAtTheScreen: function() {
    			return this;
    		},
    
    		createAWaitForAnEntitySet: function(oOptions) {
    			return {
    				success: function() {
    					var bMockServerAvailable = false,
    						aEntitySet;
    
    					this.getMockServer().then(function(oMockServer) {
    						aEntitySet = oMockServer.getEntitySetData(oOptions.entitySet);
    						bMockServerAvailable = true;
    					});
    
    					return this.waitFor({
    						check: function() {
    							return bMockServerAvailable;
    						},
    						success: function() {
    							oOptions.success.call(this, aEntitySet);
    						}
    					});
    				}
    			};
    		},
    
    		getMockServer: function() {
    			return new Promise(function(success) {
    				(Opa5.getWindow() || window).sap.ui.require(["com/acme/top/wdsp/mon/localService/mockserver"], function(mockserver) {
    					success(mockserver.getMockServer());
    				});
    			});
    		},
    
    		iTeardownMyApp: function() {
    			var oOptions = createWaitForObjectWithoutDefaults();
    			oOptions.success = function() {
    				if (launcher.hasLaunched()) {
    					this.iTeardownMyUIComponentInUshell();
    				} else {
    					Opa5.prototype.iTeardownMyApp.apply(this, arguments);
    				}
    			}.bind(this);
    
    			return this.waitFor(oOptions);
    		},
    
    		iTeardownMyUIComponentInUshell: function() {
    
    			var oOptions = createWaitForObjectWithoutDefaults();
    			oOptions.success = function() {
    				launcher.teardown();
    			};
    			return this.waitFor(oOptions);
    		}
    	});
    });​
  8. The test runs and code coverage results are now shown, the app runs in the Unified Shell (ushell):

Issue 2: Karma context for OPA5 integration tests

Karma can be used to automate OPA5 tests. As long as tests are not run in an iframe, code coverage reporting works as expected. But karma-openui5 <= 0.2.3 can unfortunately not be used to set up the test context of a launchpad app, because of a problem with loading the ‘sap.ushell’ library when the dependency is given as window[“sap-ui-config”].libs = “sap.m, sap.ushell” (instead of data-sap-ui-libs=’sap.m, sap.ushell’ – see comments in code for ‘opaTestsWithComponent.qunit.html’ above).

How to the up the Karma test context for OPA5 integration tests for launchpad apps?

  1. package.json:
    {
      "name": "com.acme.top.wdsp.mon",
      "version": "1.0.0",
      "description": "Monitor Tool",
      "main": "webapp/Component.js",
      "license": "UNLICENSED",
      "scripts": {
        "karma": "karma start karma.conf.js",
        "test": "npm run karma:ci",
        "karma:ci": "karma start karma.conf.js --singleRun"
      },
      "author": "Laszlo Kajan",
      "devDependencies": {
        "karma": "^2.0.5",
        "karma-chrome-launcher": "^2.2.0",
        "karma-cli": "^1.0.1",
        "karma-coverage": "^1.1.2",
        "karma-junit-reporter": "^1.2.0",
        "karma-phantomjs-launcher": "^1.0.4",
        "karma-qunit": "^2.1.0",
        "qunit": "^2.6.1"
      }
    }​
  2. karma.conf.js:
    // Example: Headless OPA5 testing with Karma and PhantomJS
    //  https://blogs.sap.com/2016/11/21/headless-opa5-testing-with-karma-and-phantomjs/
    module.exports = function(config) {
      'use strict';
    
      const CI_MODE = !!config.singleRun;
    
      config.set({
        // frameworks to use
        // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
        frameworks: [
          'qunit'
        ],
    
        // list of files / patterns to load in the browser
        files: [
          {pattern: 'node_modules/mobx/lib/mobx.umd.min.js',  included: false, served: true, watched: false},
          {pattern: 'com.acme.top.eihb/webapp/**/*',    included: false, served: true, watched: true},
          {pattern: 'sap.ui.mobx/src/**/*',                   included: false, served: true, watched: true},
          {pattern: 'webapp/**/*',                            included: false, served: true, watched: true},
        ],
    
        // list of files / patterns to exclude
        exclude: [
        ],
    
        customContextFile: "webapp/test/karma/context.html",
    
        client: {
          captureConsole: true,
          // If false, Karma does not clear the context window upon the completion of running the tests
          clearContext: false,
          useIframe: false,
          qunit: {
            // showUI: true needs the clearContext: false option to display correctly in non-debug mode
            //showUI: true,
            testTimeout: 15000,
            autostart: false,
            autoload: false
          }
        },
    
        proxies: {
          '/sap/bc/bsp/sap/zx6g_libmobx/4.1.1/': '/base/node_modules/mobx/lib/',
          '/sap/bc/ui5_ui5/sap/zf67_eihb_lib/': '/base/com.acme.top.eihb/webapp/',
          '/sap/bc/ui5_ui5/sap/zx6g_libmobxui5/': '/base/sap.ui.mobx/src/'
        },
     
        // preprocess matching files before serving them to the browser
        // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
        preprocessors: CI_MODE ? {
          'webapp/*.js':                          ['coverage'],
          'webapp/!(localService|test)/**/*.js':  ['coverage']
        } : {},
    
        // test results reporter to use
        // possible values: 'dots', 'progress'
        // available reporters: https://npmjs.org/browse/keyword/karma-reporter
        reporters: ['progress', 'junit'].concat(CI_MODE ? 'coverage' : []),
    
        coverageReporter: {
          dir: 'reports/coverage',
          // Include all sources files, as indicated by the coverage preprocessor
          includeAllSources: true,
          subdir: browser => browser.split(' ')[0],
          reporters: [
    //        {type: 'cobertura', subdir: 'cobertura'},
            {type: 'lcov', subdir: 'lcov'},
    //        {type: 'lcovonly', subdir: 'lcovonly'},
            {type: 'text-summary'}
          ]
        },
    
        junitReporter: {
          outputDir:      'reports',  // results will be saved as $outputDir/$browserName.xml 
          outputFile:     'junit/webapp.xml',
                                      // if included, results will be saved as $outputDir/$browserName/$outputFile 
          suite:          'sapui5',  // suite will become the package name attribute in xml testsuite element
          useBrowserName: true        // add browser name to report and classes names 
        },
    
        // enable / disable colors in the output (reporters and logs)
        colors: true,
    
        // level of logging
        // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
        logLevel: config.LOG_ERROR,
    
        //loggers: [
        //  {type: 'console'},
        //  {
        //    type:         'file',
        //    filename:     'karma.log',
        //    maxLogSize:   65536,
        //    backups:      3
        //  }
        //],
    
        browserNoActivityTimeout: 30000,
    
        // start these browsers
        // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
        browsers: [
    //      'Chrome'
          'Chrome_without_security'
    //      'PhantomJS_custom'
        ],
    
        customLaunchers: {
          Chrome_without_security: {
            base: 'Chrome',
            flags: ['--disable-web-security']
          },
    
          PhantomJS_custom: {
            base: 'PhantomJS',
            options: {
              viewportSize: {
                  width: 1920,
                  height: 1080
              },
              customHeaders: {
                  DNT: "1"
              },
              windowName: 'my-window',
              settings: {
                  webSecurityEnabled: false,
                  userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.87 Safari/537.36"
              }
            },
            flags: ['--load-images=true', '--debug=false', '--disk-cache=false'],
            debug: false
          }
        },
    
        // Have phantomjs exit if a ResourceError is encountered (useful if karma exits without killing phantom)
        phantomjsLauncher: {
          exitOnResourceError: false
        },
    
        // enable / disable watching file and executing tests whenever any file changes
        // Use `karma run' to run tests if set to false
        autoWatch: true,
     
        // Continuous Integration mode
        // if true, Karma captures browsers, runs the tests and exits
        singleRun: true,
    
        // Concurrency level
        // how many browser should be started simultaneous
        concurrency: Infinity
      })
    }
    // vim:et:ts=2:​
  3. webapp/test/karma/context.html:
    <!DOCTYPE html>
    <!--
    Copy of node_modules/karma/static/context.html
    This is the execution context.
    Reloaded before every execution run.
    -->
    <html>
    <head>
      <title>Integration tests for com.acme.top.wdsp.mon with Component</title>
      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
      <meta http-equiv='X-UA-Compatible' content='IE=edge'>
      <meta charset="utf-8">
    </head>
    <body>
      <!-- The scripts need to be in the body DOM element, as some test running frameworks need the body
           to have already been created so they can insert their magic into it. For example, if loaded
           before body, Angular Scenario test framework fails to find the body and crashes and burns in
           an epic manner. -->
      <script src="context.js"></script>
      <script type="text/javascript">
        // Configure our Karma and set up bindings
        %CLIENT_CONFIG%
        window.__karma__.setupContext(window);
    
        // All served files with the latest timestamps
        %MAPPINGS%
      </script>
    		<!-- copy from opaTestsWithComponent.qunit.html { -->
    		<script type="text/javascript">
    			window["sap-ushell-config"] = {
    				defaultRenderer : "fiori2",
    				renderers: {
    					fiori2: {
    						componentData: {
    							config: {
    								rootIntent: "ZF67_WDAYSP-monitor",
    								search: "hidden"
    							}
    						}
    					}
    				},
    				applications: {
    					"ZF67_WDAYSP-monitor": {
    						"additionalInformation": "SAPUI5.Component=com.acme.top.wdsp.mon",
    						"applicationType": "URL",
    						"url": "/base/webapp/"
    					}
    				}
    			};
    		</script>
    		<script src="https://sapui5.hana.ondemand.com/test-resources/sap/ushell/bootstrap/sandbox.js" id="sap-ushell-bootstrap"></script>
    
    				<script id="sap-ui-bootstrap"
    				src="https://sapui5.hana.ondemand.com/1.44.38/resources/sap-ui-core.js"
    				data-sap-ui-animation="false"
    				data-sap-ui-compatVersion="edge"
    				data-sap-ui-frameOptions='deny'
    				data-sap-ui-language="en"
    				data-sap-ui-libs='sap.m, sap.ushell'
    				data-sap-ui-preload='async'
    				data-sap-ui-resourceroots='{
    					"com.acme.top.wdsp.mon" : "/base/webapp/",
    					"Arrangement": "/base/webapp/test/integration/arrangement/component/Arrangement"
    				}'
    				data-sap-ui-theme="sap_belize"
    				data-sap-ui-xx-supportedLanguages="">
    		</script>
    
    		<!-- copy from opaTestsWithComponent.qunit.html } -->
      <!-- Dynamically replaced with <script> tags -->
      %SCRIPTS%
    		<!-- copy from opaTestsWithComponent.qunit.html { -->
    		<script src="https://sapui5.hana.ondemand.com/1.44.38/resources/sap/ui/qunit/qunit-css.js"></script>
    		<script src="https://sapui5.hana.ondemand.com/1.44.38/resources/sap/ui/thirdparty/qunit.js"></script>
    		<script src="https://sapui5.hana.ondemand.com/1.44.38/resources/sap/ui/qunit/qunit-junit.js"></script>
    		<script src="https://sapui5.hana.ondemand.com/1.44.38/resources/sap/ui/qunit/qunit-coverage.js"
    			data-sap-ui-cover-only="com/acme/top/wdsp/mon/"
    			data-sap-ui-cover-never="[com/acme/top/wdsp/mon/localService/, com/acme/top/wdsp/mon/test/]">
    			// TODO: data-sap-ui-cover-only="com/acme/top/wdsp/mon/" may be too narrow: include libraries as well?
    		</script>
    
    		<script>
    			// https://github.com/SAP/openui5/blob/master/src/sap.m/test/sap/m/demokit/cart/webapp/test/integration/opaTestsWithComponent.qunit.html
    			// we want to be able to load our tests asynchronously - pause QUnit until we loaded everything
    			QUnit.config.autostart = false;
    
    			sap.ui.getCore().attachInit(function () {
    				"use strict";
    				sap.ui.require([
    					"sap/ui/fl/FakeLrepConnector",
    					"com/acme/top/wdsp/mon/localService/mockserver",
    					"com/acme/top/wdsp/mon/test/integration/AllJourneys"
    				], function (FakeLrepConnector, server) {
    					//Fake LREP
    					FakeLrepConnector.enableFakeConnector("/base/webapp/fakeLRep.json");
    
    					// set up test service for local testing
    					server.init({autoRespondAfter: 50});
    
    					// configuration has been applied and the tests in the journeys have been loaded - start QUnit
    					QUnit.start();
    				});
    			});
    		</script>
    		<!-- copy from opaTestsWithComponent.qunit.html } -->
      <script type="text/javascript">
        window.__karma__.loaded();
      </script>
    </body>
    </html>​
  4. Run the tests like this:
    #!/bin/sh -e
    export PATH=./node_modules/.bin:$PATH;
    export CHROME_BIN=chromium;
    npm install;
    Xvfb :99 & export XVFB_PID=$!;
    export DISPLAY=:99.0;
    npm test;​
  5. Read the reports:
    1. junit: reports/*/junit/webapp.xml
    2. coverage: reports/coverage/lcov/lcov-report/index.html

Summary

This blog post showed you how to run OPA5 integration tests for your Fiori launchpad app.

You are now also able to automate the running of the tests using Karma.

Further reading

Afterword

Thank you for reading through this blog post. I hope you found it useful. I hope it raised questions as well. Do follow the above, and other links to satisfy your curiosity – a sure way to deepen your understanding.

Please check out my other blog posts for interesting topics, like “Adding Reactive State Management with Validation to Existing UI5 Application – a Tutorial“.

Assigned Tags

      4 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Laszlo Kajan
      Laszlo Kajan
      Blog Post Author

      Just tried it for another app, and worked perfectly.

      Author's profile photo pas longo
      pas longo

      Hi Laszlo,

      terrific article.

      we are trying to adapt your example to use with GitLab CICD using Grunt to run the tests.

      we seem to be experiencing major issues - the tests seem to run but they all seem to time out waiting for the mock data to be loaded to the various controls in our test app.

      Would it be possible to get a copy of your app code so that we can explore it more better.

      thank you in advance.

      cheers.

      Pas.

       

      Author's profile photo Laszlo Kajan
      Laszlo Kajan
      Blog Post Author

      Hello Pas!

      The mock server arrangement is the standard one.

      Make sure your mock server is started, before you run the tests – check out webapp/test/karma/context.html above.

      I will make a sample app available when I find the time.

      Best regards,

      Laszlo

      Author's profile photo pas longo
      pas longo

      Hi Laszlo,

      Thanks for the reply.

      have studied your code a bit more, have reviewed our code and it turns out that we were not setting the binding context to 'complex' in the boot strap.

      once again, thanks for your reply.

      Cheers

      Pas.