├── .gitignore
├── bs-config.json
├── .editorconfig
├── .gitattributes
├── src
├── view
│ ├── App.view.xml
│ ├── NotFound.view.xml
│ ├── DetailObjectNotFound.view.xml
│ ├── DetailNoObjectsAvailable.view.xml
│ ├── ViewSettingsDialog.fragment.xml
│ ├── Detail.view.xml
│ └── Master.view.xml
├── test
│ ├── unit
│ │ ├── allTests.js
│ │ ├── model
│ │ │ ├── formatter.js
│ │ │ ├── GroupSortState.js
│ │ │ ├── models.js
│ │ │ └── grouper.js
│ │ ├── unitTests.qunit.html
│ │ └── controller
│ │ │ ├── App.controller.js
│ │ │ └── ListSelector.js
│ ├── testsuite.qunit.html
│ ├── integration
│ │ ├── opaTests.qunit.html
│ │ ├── opaTestsPhone.qunit.html
│ │ ├── BusyJourneyPhone.js
│ │ ├── BusyJourney.js
│ │ ├── PhoneJourneys.js
│ │ ├── AllJourneys.js
│ │ ├── NavigationJourneyPhone.js
│ │ ├── NotFoundJourneyPhone.js
│ │ ├── NotFoundJourney.js
│ │ ├── pages
│ │ │ ├── App.js
│ │ │ ├── Browser.js
│ │ │ ├── Common.js
│ │ │ ├── NotFound.js
│ │ │ ├── Detail.js
│ │ │ └── Master.js
│ │ ├── NavigationJourney.js
│ │ └── MasterJourney.js
│ └── mockServer.html
├── model
│ ├── models.ts
│ ├── formatter.ts
│ ├── grouper.ts
│ └── GroupSortState.ts
├── test.html
├── controller
│ ├── App.controller.ts
│ ├── ErrorHandler.ts
│ ├── BaseController.ts
│ ├── ListSelector.ts
│ ├── Detail.controller.ts
│ └── Master.controller.js
├── index.html
├── Component.ts
├── localService
│ ├── mockserver.ts
│ ├── mockdata
│ │ ├── Objects.json
│ │ └── LineItems.json
│ └── metadata.xml
├── i18n
│ └── i18n.properties
└── manifest.json
├── .vscode
└── settings.json
├── package.json
├── tsconfig.json
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /resources
3 | *.js
4 | *.js.map
5 |
--------------------------------------------------------------------------------
/bs-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "port": 8000,
3 | "server": {
4 | "baseDir": "src",
5 | "routes": {
6 | "/node_modules": "node_modules",
7 | "/resources": "resources"
8 | }
9 | }
10 | }
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 |
2 | root = true
3 |
4 | [{src}/**.{html,xml,css,json,js,ts,properties}]
5 | end_of_line = lf
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 | indent_style = space
10 | indent_size = 4
11 | tab_width = 4
12 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
3 | *.html text eol=lf
4 | *.xml text eol=lf
5 | *.css text eol=lf
6 | *.json text eol=lf
7 | *.js text eol=lf
8 | *.ts text eol=lf
9 | *.properties text eol=lf
10 |
11 | *.png binary
12 | *.ico binary
13 |
--------------------------------------------------------------------------------
/src/view/App.view.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
--------------------------------------------------------------------------------
/src/test/unit/allTests.js:
--------------------------------------------------------------------------------
1 | sap.ui.define([
2 | "test/unit/model/models",
3 | "test/unit/model/formatter",
4 | "test/unit/controller/App.controller",
5 | "test/unit/controller/ListSelector",
6 | "test/unit/model/grouper",
7 | "test/unit/model/GroupSortState"
8 | ], function() {
9 | "use strict";
10 | });
--------------------------------------------------------------------------------
/src/model/models.ts:
--------------------------------------------------------------------------------
1 | import JSONModel from "sap/ui/model/json/JSONModel";
2 | import Device from "sap/ui/Device";
3 |
4 | export default {
5 | createDeviceModel(): JSONModel {
6 | //TODO|ui5ts: generate constructors
7 | var oModel = new JSONModel(Device);
8 | oModel.setDefaultBindingMode(sap.ui.model.BindingMode.OneWay);
9 | return oModel;
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.tabSize": 4,
3 | "editor.insertSpaces": true,
4 | "editor.detectIndentation": false,
5 | "files.exclude": {
6 | "**/.git": true,
7 | "**/*.js.map": true,
8 | "**/*.js": {
9 | "when": "$(basename).ts"
10 | },
11 | "node_modules": true,
12 | "resources": true
13 | },
14 | "ui5ts.manifestlocation": "src/manifest.json"
15 | }
--------------------------------------------------------------------------------
/src/model/formatter.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | /**
3 | * Rounds the currency value to 2 digits
4 | *
5 | * @public
6 | * @param {string} sValue value to be formatted
7 | * @returns {string} formatted currency value with 2 digits
8 | */
9 | currencyValue : function (sValue?: string): string {
10 | if (!sValue) {
11 | return "";
12 | }
13 |
14 | return parseFloat(sValue).toFixed(2);
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/src/view/NotFound.view.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/view/DetailObjectNotFound.view.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/view/DetailNoObjectsAvailable.view.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ui-typescript-example",
3 | "version": "1.0.0",
4 | "scripts": {
5 | "start": "concurrently \"npm run build:watch\" \"npm run serve\"",
6 | "serve": "lite-server -c=bs-config.json",
7 | "build": "tsc -p tsconfig.json",
8 | "build:watch": "tsc -p tsconfig.json -w",
9 | "lint": "tslint ./src/**/*.ts -t verbose"
10 | },
11 | "dependencies": {
12 | "ui5ts": "^0.3.0"
13 | },
14 | "devDependencies": {
15 | "@types/es6-shim": "^0.31.35",
16 | "@types/jquery": "^2.0.47",
17 | "concurrently": "^3.2.0",
18 | "lite-server": "^2.3.0",
19 | "tslint": "^3.15.1",
20 | "typescript": "^2.5.2"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/test/testsuite.qunit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | QUnit TestSuite for Master-Detail
5 |
6 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "amd",
5 | "experimentalDecorators": true,
6 | "alwaysStrict": true,
7 | "noImplicitAny": true,
8 | "strictNullChecks": true,
9 | "noImplicitReturns": true,
10 | "noImplicitThis": true,
11 | "sourceMap": true,
12 | "baseUrl": "./",
13 | "paths": {
14 | "typescript/example/ui5app/*": [ "./src/*" ],
15 | "sap/*": [ "./node_modules/ui5ts/exports/sap/*" ]
16 | }
17 | },
18 | "files": [
19 | "node_modules/ui5ts/ui5ts.d.ts",
20 | "node_modules/ui5ts/ui5-types/1.48/sap.d.ts",
21 | "node_modules/ui5ts/ui5-types/1.48/jQuery.d.ts"
22 | ],
23 | "include": [
24 | "src/**/*",
25 | "node_modules/@types"
26 | ],
27 | "exclude": [
28 | "node_modules",
29 | "resources",
30 | "**/*.spec.ts"
31 | ]
32 | }
--------------------------------------------------------------------------------
/src/test/integration/opaTests.qunit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Opa tests for Master-Detail
5 |
6 |
7 |
8 |
15 |
16 |
17 |
18 |
19 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/test/integration/opaTestsPhone.qunit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Opa tests for Master-Detail
5 |
6 |
7 |
8 |
15 |
16 |
17 |
18 |
19 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/view/ViewSettingsDialog.fragment.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
9 |
10 |
15 |
16 |
20 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/test/integration/BusyJourneyPhone.js:
--------------------------------------------------------------------------------
1 | /*global QUnit*/
2 |
3 | sap.ui.define([
4 | "sap/ui/test/opaQunit"
5 | ], function (opaTest) {
6 | "use strict";
7 |
8 | QUnit.module("Phone busy indication");
9 |
10 | opaTest("Should see a global busy indication while loading the metadata", function (Given, When, Then) {
11 | // Arrangements
12 | Given.iStartTheAppWithDelay("", 5000);
13 |
14 | //Actions
15 | When.onTheAppPage.iLookAtTheScreen();
16 |
17 | // Assertions
18 | Then.onTheAppPage.iShouldSeeTheBusyIndicator().
19 | and.iTeardownMyAppFrame();
20 | });
21 |
22 | opaTest("Should see a busy indication on the master after loading the metadata", function (Given, When, Then) {
23 | // Arrangements
24 | Given.iStartTheAppWithDelay("", 2000);
25 |
26 | //Actions
27 | When.onTheAppPage.iWaitUntilTheBusyIndicatorIsGone();
28 |
29 | // Assertions
30 | Then.onTheMasterPage.iShouldSeeTheBusyIndicator().
31 | and.iTeardownMyAppFrame();
32 | });
33 |
34 | });
--------------------------------------------------------------------------------
/src/test/integration/BusyJourney.js:
--------------------------------------------------------------------------------
1 | /*global QUnit*/
2 |
3 | sap.ui.define([
4 | "sap/ui/test/opaQunit"
5 | ], function (opaTest) {
6 | "use strict";
7 |
8 | QUnit.module("Desktop busy indication");
9 |
10 | opaTest("Should see a global busy indication while loading the metadata", function (Given, When, Then) {
11 | // Arrangements
12 | Given.iStartTheAppWithDelay("", 5000);
13 |
14 | // Actions
15 | When.onTheAppPage.iLookAtTheScreen();
16 |
17 | // Assertions
18 | Then.onTheAppPage.iShouldSeeTheBusyIndicator().
19 | and.iTeardownMyAppFrame();
20 | });
21 |
22 | opaTest("Should see a busy indication on the master and detail after loading the metadata", function (Given, When, Then) {
23 | // Arrangements
24 | Given.iStartTheAppWithDelay("", 2000);
25 |
26 | // Actions
27 | When.onTheAppPage.iWaitUntilTheBusyIndicatorIsGone();
28 |
29 | // Assertions
30 | Then.onTheMasterPage.iShouldSeeTheBusyIndicator().
31 | and.theListHeaderDisplaysZeroHits();
32 | Then.onTheDetailPage.iShouldSeeTheBusyIndicator().
33 | and.iTeardownMyAppFrame();
34 | });
35 |
36 | });
--------------------------------------------------------------------------------
/src/test/integration/PhoneJourneys.js:
--------------------------------------------------------------------------------
1 | /*global QUnit*/
2 |
3 | jQuery.sap.require("sap.ui.qunit.qunit-css");
4 | jQuery.sap.require("sap.ui.thirdparty.qunit");
5 | jQuery.sap.require("sap.ui.qunit.qunit-junit");
6 | QUnit.config.autostart = false;
7 |
8 | sap.ui.require([
9 | "sap/ui/test/Opa5",
10 | "typescript/example/ui5app/test/integration/pages/Common",
11 | "sap/ui/test/opaQunit",
12 | "typescript/example/ui5app/test/integration/pages/App",
13 | "typescript/example/ui5app/test/integration/pages/Browser",
14 | "typescript/example/ui5app/test/integration/pages/Master",
15 | "typescript/example/ui5app/test/integration/pages/Detail",
16 | "typescript/example/ui5app/test/integration/pages/NotFound"
17 | ], function (Opa5, Common) {
18 | "use strict";
19 | Opa5.extendConfig({
20 | arrangements: new Common(),
21 | viewNamespace: "typescript.example.ui5app.view."
22 | });
23 |
24 | sap.ui.require([
25 | "typescript/example/ui5app/test/integration/NavigationJourneyPhone",
26 | "typescript/example/ui5app/test/integration/NotFoundJourneyPhone",
27 | "typescript/example/ui5app/test/integration/BusyJourneyPhone"
28 | ], function () {
29 | QUnit.start();
30 | });
31 | });
--------------------------------------------------------------------------------
/src/test/unit/model/formatter.js:
--------------------------------------------------------------------------------
1 | /*global QUnit*/
2 |
3 | sap.ui.define([
4 | "sap/m/Text",
5 | "typescript/example/ui5app/model/formatter"
6 | ], function (Text, formatter) {
7 | "use strict";
8 |
9 | QUnit.module("formatter - Currency value");
10 |
11 | function currencyValueTestCase(assert, sValue, fExpectedNumber) {
12 | // Act
13 | var fCurrency = formatter.currencyValue(sValue);
14 |
15 | // Assert
16 | assert.strictEqual(fCurrency, fExpectedNumber, "The rounding was correct");
17 | }
18 |
19 | QUnit.test("Should round down a 3 digit number", function (assert) {
20 | currencyValueTestCase.call(this, assert, "3.123", "3.12");
21 | });
22 |
23 | QUnit.test("Should round up a 3 digit number", function (assert) {
24 | currencyValueTestCase.call(this, assert, "3.128", "3.13");
25 | });
26 |
27 | QUnit.test("Should round a negative number", function (assert) {
28 | currencyValueTestCase.call(this, assert, "-3", "-3.00");
29 | });
30 |
31 | QUnit.test("Should round an empty string", function (assert) {
32 | currencyValueTestCase.call(this, assert, "", "");
33 | });
34 |
35 | QUnit.test("Should round a zero", function (assert) {
36 | currencyValueTestCase.call(this, assert, "0", "0.00");
37 | });
38 |
39 | });
--------------------------------------------------------------------------------
/src/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Testing Overview
5 |
6 |
7 |
8 |
9 | Testing Overview
10 | This is an overview page of various ways to test the generated app during development. Choose one of the access points below to launch the app as a standalone application, e.g. on a Tomcat server.
11 |
12 |
22 |
23 |
--------------------------------------------------------------------------------
/src/test/unit/unitTests.qunit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Unit tests for Master-Detail
5 |
6 |
7 |
8 |
16 |
17 |
18 |
19 |
20 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/controller/App.controller.ts:
--------------------------------------------------------------------------------
1 | import BaseController from "typescript/example/ui5app/controller/BaseController";
2 | import JSONModel from "sap/ui/model/json/JSONModel";
3 |
4 | @UI5("typescript.example.ui5app.controller.App")
5 | export default class App extends BaseController {
6 |
7 | public onInit(): void {
8 | var oViewModel: JSONModel,
9 | oComponent = this.getOwnerComponent(),
10 | oModel = oComponent.getModel(),
11 | fnSetAppNotBusy: () => void,
12 | oListSelector = oComponent.oListSelector,
13 | iOriginalBusyDelay = this.getView().getBusyIndicatorDelay();
14 |
15 | oViewModel = new JSONModel({
16 | busy : true,
17 | delay : 0
18 | });
19 | this.setModel(oViewModel, "appView");
20 |
21 | fnSetAppNotBusy = () => {
22 | oViewModel.setProperty("/busy", false);
23 | oViewModel.setProperty("/delay", iOriginalBusyDelay);
24 | };
25 |
26 | oModel.metadataLoaded()
27 | .then(fnSetAppNotBusy);
28 |
29 | // Makes sure that master view is hidden in split app
30 | // after a new list entry has been selected.
31 | oListSelector.attachListSelectionChange(() => {
32 | this.byId("idAppControl").hideMaster();
33 | }, this);
34 |
35 | // apply content density mode to root view
36 | this.getView().addStyleClass(oComponent.getContentDensityClass());
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/src/test/integration/AllJourneys.js:
--------------------------------------------------------------------------------
1 | /*global QUnit*/
2 |
3 | jQuery.sap.require("sap.ui.qunit.qunit-css");
4 | jQuery.sap.require("sap.ui.thirdparty.qunit");
5 | jQuery.sap.require("sap.ui.qunit.qunit-junit");
6 | QUnit.config.autostart = false;
7 |
8 | // We cannot provide stable mock data out of the template.
9 | // If you introduce mock data, by adding .json files in your webapp/localService/mockdata folder you have to provide the following minimum data:
10 | // * At least 3 Objects in the list
11 | // * All 3 Objects have at least one LineItems
12 |
13 | sap.ui.require([
14 | "sap/ui/test/Opa5",
15 | "typescript/example/ui5app/test/integration/pages/Common",
16 | "sap/ui/test/opaQunit",
17 | "typescript/example/ui5app/test/integration/pages/App",
18 | "typescript/example/ui5app/test/integration/pages/Browser",
19 | "typescript/example/ui5app/test/integration/pages/Master",
20 | "typescript/example/ui5app/test/integration/pages/Detail",
21 | "typescript/example/ui5app/test/integration/pages/NotFound"
22 | ], function (Opa5, Common) {
23 | "use strict";
24 | Opa5.extendConfig({
25 | arrangements: new Common(),
26 | viewNamespace: "typescript.example.ui5app.view."
27 | });
28 |
29 | sap.ui.require([
30 | "typescript/example/ui5app/test/integration/MasterJourney",
31 | "typescript/example/ui5app/test/integration/NavigationJourney",
32 | "typescript/example/ui5app/test/integration/NotFoundJourney",
33 | "typescript/example/ui5app/test/integration/BusyJourney"
34 | ], function () {
35 | QUnit.start();
36 | });
37 | });
--------------------------------------------------------------------------------
/src/model/grouper.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Use this file to implement your custom grouping functions
3 | * The predefined functions are simple examples and might be replaced by your more complex implementations
4 | * to be called with .bind() and handed over to a sap.ui.model.Sorter
5 | * return value for all your functions is an object with key-text pairs
6 | * the oContext parameter is not under your control!
7 | */
8 |
9 | export default {
10 |
11 | /**
12 | * Groups the items by a price in two groups: Lesser equal than 20 and greater than 20
13 | * This grouping function needs the resource bundle so we pass it as a dependency
14 | * @param {sap.ui.model.resource.ResourceModel} oResourceBundle the resource bundle of your i18n model
15 | * @returns {Function} the grouper function you can pass to your sorter
16 | */
17 | groupUnitNumber(oResourceBundle: sap.ui.model.resource.ResourceModel): (oContext: jQuery.sap.util.ResourceBundle) => { key: string, text: string } {
18 | return (oContext: jQuery.sap.util.ResourceBundle) => {
19 | var iPrice = oContext.getProperty("UnitNumber"),
20 | sKey,
21 | sText;
22 |
23 | if (iPrice <= 20) {
24 | sKey = "LE20";
25 | sText = oResourceBundle.getText("masterGroup1Header1");
26 | } else {
27 | sKey = "GT20";
28 | sText = oResourceBundle.getText("masterGroup1Header2");
29 | }
30 |
31 | return {
32 | key: sKey,
33 | text: sText
34 | };
35 | };
36 | }
37 |
38 | };
--------------------------------------------------------------------------------
/src/test/integration/NavigationJourneyPhone.js:
--------------------------------------------------------------------------------
1 | /*global QUnit*/
2 |
3 | sap.ui.define([
4 | "sap/ui/test/opaQunit"
5 | ], function (opaTest) {
6 | "use strict";
7 |
8 | QUnit.module("Phone navigation");
9 |
10 | opaTest("Should see the objects list", function (Given, When, Then) {
11 | // Arrangements
12 | Given.iStartTheApp();
13 |
14 | //Actions
15 | When.onTheMasterPage.iLookAtTheScreen();
16 |
17 | // Assertions
18 | Then.onTheMasterPage.iShouldSeeTheList();
19 | Then.onTheBrowserPage.iShouldSeeAnEmptyHash();
20 | });
21 |
22 | opaTest("Should react on hashchange", function (Given, When, Then) {
23 | // Actions
24 | When.onTheMasterPage.iRememberTheIdOfListItemAtPosition(3);
25 | When.onTheBrowserPage.iChangeTheHashToTheRememberedItem();
26 |
27 | // Assertions
28 | Then.onTheDetailPage.iShouldSeeTheRememberedObject();
29 | });
30 |
31 | opaTest("Detail Page Shows Object Details", function (Given, When, Then) {
32 | // Actions
33 | When.onTheDetailPage.iLookAtTheScreen();
34 |
35 | // Assertions
36 | Then.onTheDetailPage.iShouldSeeTheObjectLineItemsList().
37 | and.theLineItemsListShouldHaveTheCorrectNumberOfItems().
38 | and.theLineItemsHeaderShouldDisplayTheAmountOfEntries();
39 | });
40 |
41 | opaTest("Should navigate on press", function (Given, When, Then) {
42 | // Actions
43 | When.onTheDetailPage.iPressTheBackButton();
44 | When.onTheMasterPage.iRememberTheIdOfListItemAtPosition(2).
45 | and.iPressOnTheObjectAtPosition(2);
46 |
47 | // Assertions
48 | Then.onTheDetailPage.iShouldSeeTheRememberedObject().
49 | and.iTeardownMyAppFrame();
50 | });
51 |
52 | });
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Master-Detail
8 |
9 |
10 |
19 |
20 |
21 |
22 |
23 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/src/test/mockServer.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Master-Detail
8 |
9 |
10 |
19 |
20 |
21 |
22 |
23 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/src/test/unit/model/GroupSortState.js:
--------------------------------------------------------------------------------
1 | /*global QUnit*/
2 |
3 | sap.ui.define([
4 | "typescript/example/ui5app/model/GroupSortState",
5 | "sap/ui/model/json/JSONModel"
6 | ], function (GroupSortState, JSONModel) {
7 | "use strict";
8 |
9 | QUnit.module("GroupSortState - grouping and sorting", {
10 | beforeEach: function () {
11 | this.oModel = new JSONModel({});
12 | // System under test
13 | this.oGroupSortState = new GroupSortState(this.oModel, function() {});
14 | }
15 | });
16 |
17 | QUnit.test("Should always return a sorter when sorting", function (assert) {
18 | // Act + Assert
19 | assert.strictEqual(this.oGroupSortState.sort("UnitNumber").length, 1, "The sorting by UnitNumber returned a sorter");
20 | assert.strictEqual(this.oGroupSortState.sort("Name").length, 1, "The sorting by Name returned a sorter");
21 | });
22 |
23 | QUnit.test("Should return a grouper when grouping", function (assert) {
24 | // Act + Assert
25 | assert.strictEqual(this.oGroupSortState.group("UnitNumber").length, 1, "The group by UnitNumber returned a sorter");
26 | assert.strictEqual(this.oGroupSortState.group("None").length, 0, "The sorting by None returned no sorter");
27 | });
28 |
29 |
30 | QUnit.test("Should set the sorting to UnitNumber if the user groupes by UnitNumber", function (assert) {
31 | // Act + Assert
32 | this.oGroupSortState.group("UnitNumber");
33 | assert.strictEqual(this.oModel.getProperty("/sortBy"), "UnitNumber", "The sorting is the same as the grouping");
34 | });
35 |
36 | QUnit.test("Should set the grouping to None if the user sorts by Name and there was a grouping before", function (assert) {
37 | // Arrange
38 | this.oModel.setProperty("/groupBy", "UnitNumber");
39 |
40 | this.oGroupSortState.sort("Name");
41 |
42 | // Assert
43 | assert.strictEqual(this.oModel.getProperty("/groupBy"), "None", "The grouping got reset");
44 | });
45 | });
--------------------------------------------------------------------------------
/src/test/unit/controller/App.controller.js:
--------------------------------------------------------------------------------
1 | /*global QUnit,sinon*/
2 |
3 | sap.ui.define([
4 | "typescript/example/ui5app/controller/App.controller",
5 | "sap/m/SplitApp",
6 | "sap/ui/core/Control",
7 | "sap/ui/model/json/JSONModel",
8 | "sap/ui/thirdparty/sinon",
9 | "sap/ui/thirdparty/sinon-qunit"
10 | ], function(AppController, SplitApp, Control, JSONModel) {
11 | "use strict";
12 |
13 | QUnit.module("AppController - Hide master");
14 |
15 | QUnit.test("Should hide the master of a SplitApp when selection in the list changes", function (assert) {
16 | // Arrange
17 | var fnOnSelectionChange,
18 | oViewStub = new Control(),
19 | oODataModelStub = new JSONModel(),
20 | oComponentStub = new Control(),
21 | oSplitApp = new SplitApp(),
22 | fnHideMasterSpy = sinon.spy(oSplitApp,"hideMaster");
23 |
24 | oComponentStub.oListSelector = {
25 | attachListSelectionChange : function (fnFunctionToCall, oListener) {
26 | fnOnSelectionChange = fnFunctionToCall.bind(oListener);
27 | }
28 | };
29 | oComponentStub.getContentDensityClass = jQuery.noop;
30 |
31 | oODataModelStub.metadataLoaded = function () {
32 | return {
33 | then : jQuery.noop
34 | };
35 | };
36 | oComponentStub.setModel(oODataModelStub);
37 |
38 | // System under Test
39 | var oAppController = new AppController();
40 |
41 | this.stub(oAppController, "byId").withArgs("idAppControl").returns(oSplitApp);
42 | this.stub(oAppController, "getView").returns(oViewStub);
43 | this.stub(oAppController, "getOwnerComponent").returns(oComponentStub);
44 |
45 | // Act
46 | oAppController.onInit();
47 | assert.ok(fnOnSelectionChange, "Did register to the change event of the ListSelector");
48 | // Simulate the event of the list
49 | fnOnSelectionChange();
50 |
51 | // Assert
52 | assert.strictEqual(fnHideMasterSpy.callCount, 1, "Did hide the master");
53 | });
54 |
55 | });
--------------------------------------------------------------------------------
/src/test/unit/model/models.js:
--------------------------------------------------------------------------------
1 | /*global QUnit*/
2 |
3 | sap.ui.define([
4 | "typescript/example/ui5app/model/models",
5 | "sap/ui/thirdparty/sinon",
6 | "sap/ui/thirdparty/sinon-qunit"
7 | ], function (models) {
8 | "use strict";
9 |
10 | QUnit.module("createDeviceModel", {
11 | afterEach : function () {
12 | this.oDeviceModel.destroy();
13 | }
14 | });
15 |
16 | function isPhoneTestCase(assert, bIsPhone) {
17 | // Arrange
18 | this.stub(sap.ui.Device, "system", { phone : bIsPhone });
19 |
20 | // System under test
21 | this.oDeviceModel = models.createDeviceModel();
22 |
23 | // Assert
24 | assert.strictEqual(this.oDeviceModel.getData().system.phone, bIsPhone, "IsPhone property is correct");
25 | }
26 |
27 | QUnit.test("Should initialize a device model for desktop", function (assert) {
28 | isPhoneTestCase.call(this, assert, false);
29 | });
30 |
31 | QUnit.test("Should initialize a device model for phone", function (assert) {
32 | isPhoneTestCase.call(this, assert, true);
33 | });
34 |
35 | function isTouchTestCase(assert, bIsTouch) {
36 | // Arrange
37 | this.stub(sap.ui.Device, "support", { touch : bIsTouch });
38 |
39 | // System under test
40 | this.oDeviceModel = models.createDeviceModel();
41 |
42 | // Assert
43 | assert.strictEqual(this.oDeviceModel.getData().support.touch, bIsTouch, "IsTouch property is correct");
44 | }
45 |
46 | QUnit.test("Should initialize a device model for non touch devices", function (assert) {
47 | isTouchTestCase.call(this, assert, false);
48 | });
49 |
50 | QUnit.test("Should initialize a device model for touch devices", function (assert) {
51 | isTouchTestCase.call(this, assert, true);
52 | });
53 |
54 | QUnit.test("The binding mode of the device model should be one way", function (assert) {
55 |
56 | // System under test
57 | this.oDeviceModel = models.createDeviceModel();
58 |
59 | // Assert
60 | assert.strictEqual(this.oDeviceModel.getDefaultBindingMode(), "OneWay", "Binding mode is correct");
61 | });
62 |
63 | });
--------------------------------------------------------------------------------
/src/test/integration/NotFoundJourneyPhone.js:
--------------------------------------------------------------------------------
1 | /*global QUnit*/
2 |
3 | sap.ui.define([
4 | "sap/ui/test/opaQunit"
5 | ], function (opaTest) {
6 | "use strict";
7 |
8 | QUnit.module("Phone not found");
9 |
10 | opaTest("Should see the not found page if the hash is something that matches no route", function (Given, When, Then) {
11 | // Arrangements
12 | Given.iStartTheApp({ hash : "somethingThatDoesNotExist" });
13 |
14 | // Actions
15 | When.onTheNotFoundPage.iLookAtTheScreen();
16 |
17 | // Assertions
18 | Then.onTheNotFoundPage.iShouldSeeTheNotFoundPage().
19 | and.theNotFoundPageShouldSayResourceNotFound();
20 | });
21 |
22 | opaTest("Should end up on the master list, if the back button is pressed", function (Given, When, Then) {
23 | // Actions
24 | When.onTheNotFoundPage.iPressTheBackButton("NotFound");
25 |
26 | // Assertions
27 | Then.onTheMasterPage.iShouldSeeTheList().
28 | and.iTeardownMyAppFrame();
29 | });
30 |
31 | opaTest("Should see the not found master and detail page if an invalid object id has been called", function (Given, When, Then) {
32 | // Arrangements
33 | Given.iStartTheApp({ hash : "/Objects/SomeInvalidObjectId" });
34 |
35 | // Actions
36 | When.onTheNotFoundPage.iLookAtTheScreen();
37 |
38 | // Assertions
39 | Then.onTheNotFoundPage.iShouldSeeTheObjectNotFoundPage().
40 | and.theNotFoundPageShouldSayObjectNotFound();
41 | });
42 |
43 | opaTest("Should end up on the master list, if the back button is pressed", function (Given, When, Then) {
44 | // Actions
45 | When.onTheNotFoundPage.iPressTheBackButton("DetailObjectNotFound");
46 |
47 | // Assertions
48 | Then.onTheMasterPage.iShouldSeeTheList().
49 | and.iTeardownMyAppFrame();
50 | });
51 |
52 | opaTest("Should see the not found text for no search results", function (Given, When, Then) {
53 | // Arrangements
54 | Given.iStartTheApp();
55 |
56 | // Actions
57 | When.onTheMasterPage.iSearchForSomethingWithNoResults();
58 |
59 | // Assertions
60 | Then.onTheMasterPage.iShouldSeeTheNoDataTextForNoSearchResults().
61 | and.iTeardownMyAppFrame();
62 | });
63 |
64 | });
--------------------------------------------------------------------------------
/src/test/unit/model/grouper.js:
--------------------------------------------------------------------------------
1 | /*global QUnit*/
2 |
3 | sap.ui.define([
4 | "typescript/example/ui5app/model/grouper",
5 | "sap/ui/model/resource/ResourceModel",
6 | "jquery.sap.global"
7 | ], function (Grouper, ResourceModel, $) {
8 | "use strict";
9 |
10 | function createResourceModel () {
11 | return new ResourceModel({
12 | bundleUrl : [$.sap.getModulePath("typescript.example.ui5app"), "i18n/i18n.properties"].join("/")
13 | });
14 | }
15 |
16 | QUnit.module("Sorter - Grouping functions", {
17 | beforeEach : function () {
18 | this._oResourceModel = createResourceModel();
19 | },
20 | afterEach : function () {
21 | this._oResourceModel.destroy();
22 | }
23 | });
24 |
25 | function createContextObject(vValue) {
26 | return {
27 | getProperty : function () {
28 | return vValue;
29 | }
30 | };
31 | }
32 |
33 | QUnit.test("Should group a price lesser equal 20", function (assert) {
34 | // Arrange
35 | var oContextObject = createContextObject(17.2),
36 | oGrouperReturn;
37 |
38 | // System under test
39 | var fnGroup = Grouper.groupUnitNumber(this._oResourceModel.getResourceBundle());
40 |
41 | // Assert
42 | oGrouperReturn = fnGroup(oContextObject);
43 | assert.strictEqual(oGrouperReturn.key, "LE20", "The key is as expected for a low value");
44 | assert.strictEqual(oGrouperReturn.text, this._oResourceModel.getResourceBundle().getText("masterGroup1Header1"), "The group header is as expected for a low value");
45 | });
46 |
47 | QUnit.test("Should group the price", function (assert) {
48 | // Arrange
49 | var oContextObject = createContextObject(55.5),
50 | oGrouperReturn;
51 |
52 | // System under test
53 | var fnGroup = Grouper.groupUnitNumber(this._oResourceModel.getResourceBundle());
54 |
55 | // Assert
56 | oGrouperReturn = fnGroup(oContextObject);
57 | assert.strictEqual(oGrouperReturn.key, "GT20", "The key is as expected for a high value");
58 | assert.strictEqual(oGrouperReturn.text, this._oResourceModel.getResourceBundle().getText("masterGroup1Header2"), "The group header is as expected for a high value");
59 | });
60 |
61 | });
--------------------------------------------------------------------------------
/src/test/integration/NotFoundJourney.js:
--------------------------------------------------------------------------------
1 | /*global QUnit*/
2 |
3 | sap.ui.define([
4 | "sap/ui/test/opaQunit"
5 | ], function (opaTest) {
6 | "use strict";
7 |
8 | QUnit.module("Desktop not found");
9 |
10 | opaTest("Should see the resource not found page and no selection in the master list when navigating to an invalid hash", function (Given, When, Then) {
11 | //Arrangement
12 | Given.iStartTheApp();
13 |
14 | //Actions
15 | When.onTheMasterPage.iWaitUntilTheListIsLoaded()
16 | .and.iWaitUntilTheFirstItemIsSelected();
17 | When.onTheBrowserPage.iChangeTheHashToSomethingInvalid();
18 |
19 | // Assertions
20 | Then.onTheNotFoundPage.iShouldSeeTheNotFoundPage().
21 | and.theNotFoundPageShouldSayResourceNotFound();
22 | Then.onTheMasterPage.theListShouldHaveNoSelection().
23 | and.iTeardownMyAppFrame();
24 | });
25 |
26 | opaTest("Should see the not found page if the hash is something that matches no route", function (Given, When, Then) {
27 | // Arrangements
28 | Given.iStartTheApp({ hash : "somethingThatDoesNotExist" });
29 |
30 | // Actions
31 | When.onTheNotFoundPage.iLookAtTheScreen();
32 |
33 | // Assertions
34 | Then.onTheNotFoundPage.iShouldSeeTheNotFoundPage().
35 | and.theNotFoundPageShouldSayResourceNotFound().
36 | and.iTeardownMyAppFrame();
37 | });
38 |
39 | opaTest("Should see the not found master and detail page if an invalid object id has been called", function (Given, When, Then) {
40 | // Arrangements
41 | Given.iStartTheApp({ hash : "/Objects/SomeInvalidObjectId" });
42 |
43 | //Actions
44 | When.onTheNotFoundPage.iLookAtTheScreen();
45 |
46 | // Assertions
47 | Then.onTheNotFoundPage.iShouldSeeTheObjectNotFoundPage().
48 | and.theNotFoundPageShouldSayObjectNotFound().
49 | and.iTeardownMyAppFrame();
50 | });
51 |
52 | opaTest("Should see the not found text for no search results", function (Given, When, Then) {
53 | // Arrangements
54 | Given.iStartTheApp();
55 |
56 | //Actions
57 | When.onTheMasterPage.iSearchForSomethingWithNoResults();
58 |
59 | // Assertions
60 | Then.onTheMasterPage.iShouldSeeTheNoDataTextForNoSearchResults().
61 | and.iTeardownMyAppFrame();
62 | });
63 |
64 | });
--------------------------------------------------------------------------------
/src/test/integration/pages/App.js:
--------------------------------------------------------------------------------
1 | sap.ui.define([
2 | "sap/ui/test/Opa5",
3 | "typescript/example/ui5app/test/integration/pages/Common",
4 | "sap/ui/test/matchers/PropertyStrictEquals"
5 | ], function(Opa5, Common, PropertyStrictEquals) {
6 | "use strict";
7 |
8 | var sViewName = "App",
9 | sAppControl = "idAppControl";
10 |
11 | Opa5.createPageObjects({
12 | onTheAppPage : {
13 | baseClass : Common,
14 |
15 | actions : {
16 |
17 | iWaitUntilTheBusyIndicatorIsGone : function () {
18 | return this.waitFor({
19 | id : sAppControl,
20 | viewName : sViewName,
21 | // inline-matcher directly as function
22 | matchers : function(oRootView) {
23 | // we set the view busy, so we need to query the parent of the app
24 | return oRootView.getParent().getBusy() === false;
25 | },
26 | errorMessage : "The app is still busy."
27 | });
28 | }
29 |
30 | },
31 |
32 | assertions : {
33 |
34 | iShouldSeeTheBusyIndicator : function () {
35 | return this.waitFor({
36 | id : sAppControl,
37 | viewName : sViewName,
38 | success : function (oRootView) {
39 | // we set the view busy, so we need to query the parent of the app
40 | Opa5.assert.ok(oRootView.getParent().getBusy(), "The app is busy");
41 | },
42 | errorMessage : "The app is not busy."
43 | });
44 | },
45 |
46 | iShouldSeeTheMessageBox : function () {
47 | return this.waitFor({
48 | searchOpenDialogs: true,
49 | controlType: "sap.m.Dialog",
50 | matchers : new PropertyStrictEquals({ name: "type", value: "Message"}),
51 | success: function () {
52 | Opa5.assert.ok(true, "The correct MessageBox was shown");
53 | }
54 | });
55 | }
56 |
57 | }
58 |
59 | }
60 |
61 | });
62 |
63 | });
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ui5-typescript-example
2 | A Master-Detail demo app (the same available in SAPUI5/OpenUI5 SDK) using TypeScript with npm ui5ts package.
3 |
4 | ## How to run
5 |
6 | ```
7 | git clone https://github.com/lmcarreiro/ui5-typescript-example.git
8 | cd ui5-typescript-example
9 | npm install
10 | npm start
11 | ```
12 |
13 | ## UI5 TypeScript example class
14 |
15 | ```typescript
16 | import UIComponent from "sap/ui/core/UIComponent";
17 | import models from "typescript/example/ui5app/model/models";
18 |
19 | @UI5("typescript.example.ui5app.Component")
20 | export default class Component extends UIComponent
21 | {
22 | public static metadata: any = {
23 | manifest : "json"
24 | };
25 |
26 | public init(): void {
27 | // set the device model
28 | this.setModel(models.createDeviceModel(), "device");
29 | // call the base component's init function and create the App view
30 | super.init();
31 | // create the views based on the url/hash
32 | this.getRouter().initialize();
33 | }
34 | }
35 | ```
36 |
37 | ## Progress
38 |
39 | I've published an incomplete work, not all classes of this example app was converted to typescript yet. But it is running fine, without
40 | error, and the core idea of working ui5 with typescript is the npm ui5ts package, that already
41 | works.
42 |
43 | I hope that the classes in this example that is already converted to typescript are enough for you to understand how to do it, and start
44 | using typescript in your own ui5 projects.
45 |
46 | * [ ] /controller
47 | * [x] ~~App.controller.js~~ -> App.controller.ts
48 | * [x] ~~BaseController.js~~ -> BaseController.ts
49 | * [x] ~~Detail.controller.js~~ -> Detail.controller.ts
50 | * [x] ~~ErrorHandler.js~~ -> ErrorHandler.ts
51 | * [x] ~~ListSelector.js~~ -> ListSelector.ts
52 | * [ ] Master.controller.js
53 | * [x] /localService
54 | * [x] ~~mockserver.js~~ -> mockserver.ts
55 | * [x] /model
56 | * [x] ~~formatter.js~~ -> formatter.ts
57 | * [x] ~~grouper.js~~ -> grouper.ts
58 | * [x] ~~GroupSortState.js~~ -> GroupSortState.ts
59 | * [x] ~~models.js~~ -> models.ts
60 | * [ ] /test
61 | * [x] ~~Component.js~~ -> Component.ts
62 |
63 |
64 | ## References
65 |
66 | - ui5ts npm package: https://www.npmjs.com/package/ui5ts
67 | - ui5ts github repository: https://github.com/lmcarreiro/ui5ts
68 | - OpenUI5 Master-Detail template: https://openui5.hana.ondemand.com/#/topic/8ed9339f3a99418e82a02f0fb4b5d6b9
69 | - OpenUI5 Master-Detail template repository: https://github.com/SAP/openui5-masterdetail-app
70 |
--------------------------------------------------------------------------------
/src/model/GroupSortState.ts:
--------------------------------------------------------------------------------
1 | import BaseObject from "sap/ui/base/Object";
2 | import Sorter from "sap/ui/model/Sorter";
3 |
4 | @UI5("typescript.example.ui5app.model.GroupSortState")
5 | export default class GroupSortState extends BaseObject
6 | {
7 | private _oViewModel: sap.ui.model.json.JSONModel;
8 | private _fnGroupFunction: Function;
9 |
10 | /**
11 | * Creates sorters and groupers for the master list.
12 | * Since grouping also means sorting, this class modifies the viewmodel.
13 | * If a user groups by a field, and there is a corresponding sort option, the option will be chosen.
14 | * If a user ungroups, the sorting will be reset to the default sorting.
15 | * @class
16 | * @public
17 | * @param {sap.ui.model.json.JSONModel} oViewModel the model of the current view
18 | * @param {function} fnGroupFunction the grouping function to be applied
19 | * @alias typescript.example.ui5app.model.GroupSortState
20 | */
21 | constructor(oViewModel: sap.ui.model.json.JSONModel, fnGroupFunction: Function) {
22 | super();
23 | this._oViewModel = oViewModel;
24 | this._fnGroupFunction = fnGroupFunction;
25 | }
26 |
27 | /**
28 | * Sorts by Name, or by UnitNumber
29 | *
30 | * @param {string} sKey - the key of the field used for grouping
31 | * @returns {sap.ui.model.Sorter[]} an array of sorters
32 | */
33 | sort(sKey: string): sap.ui.model.Sorter[] {
34 | var sGroupedBy = this._oViewModel.getProperty("/groupBy");
35 |
36 | if (sGroupedBy !== "None") {
37 | // If the list is grouped, remove the grouping since the user wants to sort by something different
38 | // Grouping only works if the list is primary sorted by the grouping - the first sorten contains a grouper function
39 | this._oViewModel.setProperty("/groupBy", "None");
40 | }
41 |
42 | return [new Sorter(sKey, false)];
43 | }
44 |
45 | /**
46 | * Groups by UnitNumber, or resets the grouping for the key "None"
47 | *
48 | * @param {string} sKey - the key of the field used for grouping
49 | * @returns {sap.ui.model.Sorter[]} an array of sorters
50 | */
51 | group(sKey: string): sap.ui.model.Sorter[] {
52 | var aSorters = [];
53 |
54 | if (sKey === "UnitNumber") {
55 | // Grouping means sorting so we set the select to the same Entity used for grouping
56 | this._oViewModel.setProperty("/sortBy", "UnitNumber");
57 |
58 | aSorters.push(
59 | new Sorter("UnitNumber", false,
60 | this._fnGroupFunction.bind(this))
61 | );
62 | } else if (sKey === "None") {
63 | // select the default sorting again
64 | this._oViewModel.setProperty("/sortBy", "Name");
65 | }
66 |
67 | return aSorters;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/controller/ErrorHandler.ts:
--------------------------------------------------------------------------------
1 | import UI5Object from "sap/ui/base/Object";
2 | import MessageBox from "sap/m/MessageBox";
3 | import MyUIComponent from "typescript/example/ui5app/Component";
4 |
5 | @UI5("typescript.example.ui5app.controller.ErrorHandler")
6 | export default class ErrorHandler extends UI5Object
7 | {
8 | private _oResourceModel: sap.ui.model.resource.ResourceModel;
9 | private _oResourceBundle: typeof jQuery.sap.util.ResourceBundle;
10 | private _oComponent: MyUIComponent;
11 | private _oModel: sap.ui.model.odata.v2.ODataModel;
12 | private _bMessageOpen: boolean;
13 | private _sErrorText: string;
14 |
15 | /**
16 | * Handles application errors by automatically attaching to the model events and displaying errors when needed.
17 | * @class
18 | * @param {sap.ui.core.UIComponent} oComponent reference to the app's component
19 | * @public
20 | * @alias typescript.example.ui5app.controller.ErrorHandler
21 | */
22 | public constructor(oComponent: MyUIComponent) {
23 | super();
24 | this._oResourceModel = oComponent.getModel("i18n");
25 | //TODO|ui5ts: check how to convert T|Promise into T
26 | this._oResourceBundle = this._oResourceModel.getResourceBundle();
27 | this._oComponent = oComponent;
28 | this._oModel = oComponent.getModel();
29 | this._bMessageOpen = false;
30 | this._sErrorText = this._oResourceBundle.getText("errorText");
31 |
32 | this._oModel.attachMetadataFailed((oEvent: sap.ui.base.Event) => {
33 | var oParams = oEvent.getParameters();
34 | this._showServiceError(oParams.response);
35 | }, this);
36 |
37 | this._oModel.attachRequestFailed((oEvent: sap.ui.base.Event) => {
38 | var oParams = oEvent.getParameters();
39 | // An entity that was not found in the service is also throwing a 404 error in oData.
40 | // We already cover this case with a notFound target so we skip it here.
41 | // A request that cannot be sent to the server is a technical error that we have to handle though
42 | if (oParams.response.statusCode !== "404" || (oParams.response.statusCode === 404 && oParams.response.responseText.indexOf("Cannot POST") === 0)) {
43 | this._showServiceError(oParams.response);
44 | }
45 | }, this);
46 | }
47 |
48 | /**
49 | * Shows a {@link sap.m.MessageBox} when a service call has failed.
50 | * Only the first error message will be display.
51 | * @param {string} sDetails a technical error to be displayed on request
52 | * @private
53 | */
54 | private _showServiceError(sDetails: string): void {
55 | if (this._bMessageOpen) {
56 | return;
57 | }
58 | this._bMessageOpen = true;
59 | MessageBox.error(
60 | this._sErrorText,
61 | {
62 | id : "serviceErrorMessageBox",
63 | details : sDetails,
64 | styleClass : this._oComponent.getContentDensityClass(),
65 | actions : [MessageBox.Action.CLOSE],
66 | onClose : () => {
67 | this._bMessageOpen = false;
68 | }
69 | }
70 | );
71 | }
72 | }
--------------------------------------------------------------------------------
/src/test/integration/NavigationJourney.js:
--------------------------------------------------------------------------------
1 | /*global QUnit*/
2 |
3 | sap.ui.define([
4 | "sap/ui/test/opaQunit"
5 | ], function (opaTest) {
6 | "use strict";
7 |
8 | QUnit.module("Desktop navigation");
9 |
10 | opaTest("Should start the app with empty hash: the hash should reflect the selection of the first item in the list", function (Given, When, Then) {
11 | // Arrangements
12 | Given.iStartTheApp();
13 |
14 | //Actions
15 | When.onTheMasterPage.iRememberTheSelectedItem();
16 |
17 | // Assertions
18 | Then.onTheMasterPage.theFirstItemShouldBeSelected();
19 | Then.onTheDetailPage.iShouldSeeTheRememberedObject().and.iShouldSeeNoBusyIndicator();
20 | Then.onTheBrowserPage.iShouldSeeTheHashForTheRememberedObject();
21 | });
22 |
23 | opaTest("Should react on hashchange", function (Given, When, Then) {
24 | // Actions
25 | When.onTheMasterPage.iRememberTheIdOfListItemAtPosition(2);
26 | When.onTheBrowserPage.iChangeTheHashToTheRememberedItem();
27 |
28 | // Assertions
29 | Then.onTheDetailPage.iShouldSeeTheRememberedObject().and.iShouldSeeNoBusyIndicator();
30 | Then.onTheMasterPage.theRememberedListItemShouldBeSelected();
31 | });
32 |
33 |
34 | opaTest("Should navigate on press", function (Given, When, Then) {
35 | // Actions
36 | When.onTheMasterPage.iRememberTheIdOfListItemAtPosition(1).
37 | and.iPressOnTheObjectAtPosition(1);
38 |
39 | // Assertions
40 | Then.onTheDetailPage.iShouldSeeTheRememberedObject();
41 | });
42 |
43 | opaTest("Detail Page Shows Object Details", function (Given, When, Then) {
44 | // Actions
45 | When.onTheDetailPage.iLookAtTheScreen();
46 |
47 | // Assertions
48 | Then.onTheDetailPage.iShouldSeeTheObjectLineItemsList().
49 | and.theDetailViewShouldContainOnlyFormattedUnitNumbers().
50 | and.theLineItemsListShouldHaveTheCorrectNumberOfItems().
51 | and.theLineItemsHeaderShouldDisplayTheAmountOfEntries().
52 | and.theLineItemsTableShouldContainOnlyFormattedUnitNumbers();
53 |
54 | });
55 |
56 | opaTest("Navigate to an object not on the client: no item should be selected and the object page should be displayed", function (Given, When, Then) {
57 | //Actions
58 | When.onTheMasterPage.iRememberAnIdOfAnObjectThatsNotInTheList();
59 | When.onTheBrowserPage.iChangeTheHashToTheRememberedItem();
60 |
61 | // Assertions
62 | Then.onTheDetailPage.iShouldSeeTheRememberedObject().
63 | and.iTeardownMyAppFrame();
64 | });
65 |
66 | opaTest("Start the App and simulate metadata error: MessageBox should be shown", function (Given, When, Then) {
67 | //Arrangement
68 | Given.iStartMyAppOnADesktopToTestErrorHandler("metadataError=true");
69 |
70 | // Assertions
71 | Then.onTheAppPage.iShouldSeeTheMessageBox().
72 | and.iTeardownMyAppFrame();
73 | });
74 |
75 | opaTest("Start the App and simulate bad request error: MessageBox should be shown", function (Given, When, Then) {
76 | //Arrangement
77 | Given.iStartMyAppOnADesktopToTestErrorHandler("errorType=serverError");
78 |
79 | // Assertions
80 | Then.onTheAppPage.iShouldSeeTheMessageBox().
81 | and.iTeardownMyAppFrame();
82 | });
83 |
84 | });
--------------------------------------------------------------------------------
/src/test/integration/pages/Browser.js:
--------------------------------------------------------------------------------
1 | sap.ui.define([
2 | "sap/ui/test/Opa5",
3 | "typescript/example/ui5app/test/integration/pages/Common"
4 | ], function(Opa5, Common) {
5 | "use strict";
6 |
7 | Opa5.createPageObjects({
8 | onTheBrowserPage : {
9 | baseClass : Common,
10 |
11 | actions : {
12 |
13 | iChangeTheHashToObjectN : function (iObjIndex) {
14 | return this.waitFor(this.createAWaitForAnEntitySet({
15 | entitySet : "Objects",
16 | success : function (aEntitySet) {
17 | Opa5.getHashChanger().setHash("/Objects/" + aEntitySet[iObjIndex].ObjectID);
18 | }
19 | }));
20 | },
21 |
22 | iChangeTheHashToTheRememberedItem : function () {
23 | return this.waitFor({
24 | success : function () {
25 | var sObjectId = this.getContext().currentItem.id;
26 | Opa5.getHashChanger().setHash("/Objects/" + sObjectId);
27 | }
28 | });
29 | },
30 |
31 | iChangeTheHashToSomethingInvalid : function () {
32 | return this.waitFor({
33 | success : function () {
34 | Opa5.getHashChanger().setHash("/somethingInvalid");
35 | }
36 | });
37 | }
38 |
39 | },
40 |
41 | assertions : {
42 |
43 | iShouldSeeTheHashForObjectN : function (iObjIndex) {
44 | return this.waitFor(this.createAWaitForAnEntitySet({
45 | entitySet : "Objects",
46 | success : function (aEntitySet) {
47 | var oHashChanger = Opa5.getHashChanger(),
48 | sHash = oHashChanger.getHash();
49 | Opa5.assert.strictEqual(sHash, "Objects/" + aEntitySet[iObjIndex].ObjectID, "The Hash is not correct");
50 | }
51 | }));
52 | },
53 |
54 | iShouldSeeTheHashForTheRememberedObject : function () {
55 | return this.waitFor({
56 | success : function () {
57 | var sObjectId = this.getContext().currentItem.id,
58 | oHashChanger = Opa5.getHashChanger(),
59 | sHash = oHashChanger.getHash();
60 | Opa5.assert.strictEqual(sHash, "Objects/" + sObjectId, "The Hash is not correct");
61 | }
62 | });
63 | },
64 |
65 | iShouldSeeAnEmptyHash : function () {
66 | return this.waitFor({
67 | success : function () {
68 | var oHashChanger = Opa5.getHashChanger(),
69 | sHash = oHashChanger.getHash();
70 | Opa5.assert.strictEqual(sHash, "", "The Hash should be empty");
71 | },
72 | errorMessage : "The Hash is not Correct!"
73 | });
74 | }
75 |
76 | }
77 |
78 | }
79 |
80 | });
81 |
82 | });
--------------------------------------------------------------------------------
/src/Component.ts:
--------------------------------------------------------------------------------
1 | import UIComponent from "sap/ui/core/UIComponent";
2 | import Device from "sap/ui/Device";
3 | import models from "typescript/example/ui5app/model/models";
4 | import ListSelector from "typescript/example/ui5app/controller/ListSelector";
5 | import ErrorHandler from "typescript/example/ui5app/controller/ErrorHandler";
6 |
7 | @UI5("typescript.example.ui5app.Component")
8 | export default class Component extends UIComponent
9 | {
10 | public static metadata: any = {
11 | manifest : "json"
12 | };
13 |
14 | public oListSelector: ListSelector;
15 | private _oErrorHandler: ErrorHandler;
16 | private _sContentDensityClass: string;
17 |
18 | /**
19 | * The component is initialized by UI5 automatically during the startup of the app and calls the init method once.
20 | * In this method, the device models are set and the router is initialized.
21 | * @public
22 | * @override
23 | */
24 | public init(): void {
25 | this.oListSelector = new ListSelector();
26 | this._oErrorHandler = new ErrorHandler(this);
27 |
28 | // set the device model
29 | this.setModel(models.createDeviceModel(), "device");
30 |
31 | // call the base component's init function and create the App view
32 | super.init();
33 |
34 | // create the views based on the url/hash
35 | this.getRouter().initialize();
36 | }
37 |
38 | /**
39 | * The component is destroyed by UI5 automatically.
40 | * In this method, the ListSelector and ErrorHandler are destroyed.
41 | * @public
42 | * @override
43 | */
44 | public destroy(bSuppressInvalidate: boolean): void {
45 | this.oListSelector.destroy();
46 | this._oErrorHandler.destroy();
47 |
48 | // call the base component's destroy function
49 | super.destroy(bSuppressInvalidate);
50 | }
51 |
52 | /**
53 | * This method can be called to determine whether the sapUiSizeCompact or sapUiSizeCozy
54 | * design mode class should be set, which influences the size appearance of some controls.
55 | * @public
56 | * @return {string} css class, either 'sapUiSizeCompact' or 'sapUiSizeCozy' - or an empty string if no css class should be set
57 | */
58 | public getContentDensityClass(): string {
59 | if (this._sContentDensityClass === undefined) {
60 | // check whether FLP has already set the content density class; do nothing in this case
61 | if (jQuery(document.body).hasClass("sapUiSizeCozy") || jQuery(document.body).hasClass("sapUiSizeCompact")) {
62 | this._sContentDensityClass = "";
63 | } else if (!Device.support.touch) { // apply "compact" mode if touch is not supported
64 | this._sContentDensityClass = "sapUiSizeCompact";
65 | } else {
66 | // "cozy" in case of touch support; default for most sap.m controls, but needed for desktop-first controls like sap.ui.table.Table
67 | this._sContentDensityClass = "sapUiSizeCozy";
68 | }
69 | }
70 | return this._sContentDensityClass;
71 | }
72 |
73 | /**
74 | * Convenience method for getting the model.
75 | * @public
76 | * @override
77 | * @returns the model of the component
78 | */
79 | public getModel(sName?: string): T {
80 | return super.getModel(sName);
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/test/integration/pages/Common.js:
--------------------------------------------------------------------------------
1 | sap.ui.define([
2 | "sap/ui/test/Opa5"
3 | ], function(Opa5) {
4 | "use strict";
5 |
6 | function getFrameUrl (sHash, sUrlParameters) {
7 | var sUrl = jQuery.sap.getResourcePath("typescript/example/ui5app/app", ".html");
8 | sHash = sHash || "";
9 | sUrlParameters = sUrlParameters ? "?" + sUrlParameters : "";
10 |
11 | if (sHash) {
12 | sHash = "#" + (sHash.indexOf("/") === 0 ? sHash.substring(1) : sHash);
13 | } else {
14 | sHash = "";
15 | }
16 |
17 | return sUrl + sUrlParameters + sHash;
18 | }
19 |
20 | return Opa5.extend("typescript.example.ui5app.test.integration.pages.Common", {
21 |
22 | iStartTheApp : function (oOptions) {
23 | oOptions = oOptions || {};
24 | // Start the app with a minimal delay to make tests run fast but still async to discover basic timing issues
25 | this.iStartMyAppInAFrame(getFrameUrl(oOptions.hash, "serverDelay=50"));
26 | },
27 |
28 | iStartTheAppWithDelay : function (sHash, iDelay) {
29 | this.iStartMyAppInAFrame(getFrameUrl(sHash, "serverDelay=" + iDelay));
30 | },
31 |
32 | iLookAtTheScreen : function () {
33 | return this;
34 | },
35 |
36 | iStartMyAppOnADesktopToTestErrorHandler : function (sParam) {
37 | this.iStartMyAppInAFrame(getFrameUrl("", sParam));
38 | },
39 |
40 | createAWaitForAnEntitySet : function (oOptions) {
41 | return {
42 | success: function () {
43 | var bMockServerAvailable = false,
44 | aEntitySet;
45 |
46 | this.getMockServer().then(function (oMockServer) {
47 | aEntitySet = oMockServer.getEntitySetData(oOptions.entitySet);
48 | bMockServerAvailable = true;
49 | });
50 |
51 | return this.waitFor({
52 | check: function () {
53 | return bMockServerAvailable;
54 | },
55 | success : function () {
56 | oOptions.success.call(this, aEntitySet);
57 | }
58 | });
59 | }
60 | };
61 | },
62 |
63 | getMockServer : function () {
64 | return new Promise(function (success) {
65 | Opa5.getWindow().sap.ui.require(["typescript/example/ui5app/localService/mockserver"], function (mockserver) {
66 | success(mockserver.getMockServer());
67 | });
68 | });
69 | },
70 |
71 | theUnitNumbersShouldHaveTwoDecimals : function (sControlType, sViewName, sSuccessMsg, sErrMsg) {
72 | var rTwoDecimalPlaces = /^-?\d+\.\d{2}$/;
73 |
74 | return this.waitFor({
75 | controlType : sControlType,
76 | viewName : sViewName,
77 | success : function (aNumberControls) {
78 | Opa5.assert.ok(aNumberControls.every(function(oNumberControl){
79 | return rTwoDecimalPlaces.test(oNumberControl.getNumber());
80 | }),
81 | sSuccessMsg);
82 | },
83 | errorMessage : sErrMsg
84 | });
85 | }
86 |
87 | });
88 |
89 | });
--------------------------------------------------------------------------------
/src/localService/mockserver.ts:
--------------------------------------------------------------------------------
1 | import MockServer from "sap/ui/core/util/MockServer";
2 |
3 | var oMockServer: MockServer,
4 | _sAppModulePath = "typescript/example/ui5app/",
5 | _sJsonFilesModulePath = _sAppModulePath + "localService/mockdata";
6 |
7 | export default {
8 | /**
9 | * Initializes the mock server.
10 | * You can configure the delay with the URL parameter "serverDelay".
11 | * The local mock data in this folder is returned instead of the real data for testing.
12 | * @public
13 | */
14 | init(): void {
15 | //TODO|openui5: getUriParameters parameter must be optional
16 | var oUriParameters = jQuery.sap.getUriParameters(undefined),
17 | //TODO|openui5: getModulePath 2nd parameter must be optional
18 | sJsonFilesUrl = jQuery.sap.getModulePath(_sJsonFilesModulePath, undefined),
19 | sManifestUrl = jQuery.sap.getModulePath(_sAppModulePath + "manifest", ".json"),
20 | sEntity = "Objects",
21 | sErrorParam = oUriParameters.get("errorType"),
22 | iErrorCode = sErrorParam === "badRequest" ? 400 : 500,
23 | oManifest = (jQuery.sap.syncGetJSON(sManifestUrl, undefined)).data,
24 | oMainDataSource = oManifest["sap.app"].dataSources.mainService,
25 | sMetadataUrl = jQuery.sap.getModulePath(_sAppModulePath + oMainDataSource.settings.localUri.replace(".xml", ""), ".xml"),
26 | // ensure there is a trailing slash
27 | sMockServerUrl = /.*\/$/.test(oMainDataSource.uri) ? oMainDataSource.uri : oMainDataSource.uri + "/";
28 |
29 | //TODO|ui5ts: generate constructors (with overloads when there is an optional parameter followed by a required one)
30 | oMockServer = new MockServer({
31 | rootUri : sMockServerUrl
32 | });
33 |
34 | // configure mock server with a delay of 1s
35 | MockServer.config({
36 | autoRespond : true,
37 | autoRespondAfter : (oUriParameters.get("serverDelay") || 1000)
38 | });
39 |
40 | oMockServer.simulate(sMetadataUrl, {
41 | sMockdataBaseUrl : sJsonFilesUrl,
42 | bGenerateMissingMockData : true
43 | });
44 |
45 | var aRequests = oMockServer.getRequests(),
46 | fnResponse = function (iErrCode: number, sMessage: string, aRequest: any) {
47 | aRequest.response = function(oXhr: any){
48 | oXhr.respond(iErrCode, {"Content-Type": "text/plain;charset=utf-8"}, sMessage);
49 | };
50 | };
51 |
52 | // handling the metadata error test
53 | if (oUriParameters.get("metadataError")) {
54 | aRequests.forEach( function ( aEntry ) {
55 | if (aEntry.path.toString().indexOf("$metadata") > -1) {
56 | fnResponse(500, "metadata Error", aEntry);
57 | }
58 | });
59 | }
60 |
61 | // Handling request errors
62 | if (sErrorParam) {
63 | aRequests.forEach( function ( aEntry ) {
64 | if (aEntry.path.toString().indexOf(sEntity) > -1) {
65 | fnResponse(iErrorCode, sErrorParam, aEntry);
66 | }
67 | });
68 | }
69 | oMockServer.start();
70 |
71 | jQuery.sap.log.info("Running the app with mock data");
72 | },
73 |
74 | /**
75 | * @public returns the mockserver of the app, should be used in integration tests
76 | * @returns {sap.ui.core.util.MockServer} the mockserver instance
77 | */
78 | getMockServer(): MockServer {
79 | return oMockServer;
80 | }
81 | };
82 |
--------------------------------------------------------------------------------
/src/controller/BaseController.ts:
--------------------------------------------------------------------------------
1 | /*global history */
2 | import Controller from "sap/ui/core/mvc/Controller";
3 | import History from "sap/ui/core/routing/History";
4 | import MyUIComponent from "typescript/example/ui5app/Component";
5 |
6 | @UI5("typescript.example.ui5app.controller.BaseController")
7 | export default class BaseController extends Controller
8 | {
9 | /**
10 | * Convenience method for accessing the router in every controller of the application.
11 | * @public
12 | * @returns {sap.ui.core.routing.Router} the router for this component
13 | */
14 | public getRouter(): sap.ui.core.routing.Router {
15 | return (this.getOwnerComponent()).getRouter();
16 | }
17 |
18 | /**
19 | * Convenience method for getting the view model by name in every controller of the application.
20 | * @public
21 | * @param {string} sName the model name
22 | * @returns {sap.ui.model.Model} the model instance
23 | */
24 | public getModel(sName?: string): T {
25 | return this.getView().getModel(sName);
26 | }
27 |
28 | /**
29 | * Convenience method for setting the view model in every controller of the application.
30 | * @public
31 | * @param {sap.ui.model.Model} oModel the model instance
32 | * @param {string} sName the model name
33 | * @returns {sap.ui.core.mvc.View} the view instance
34 | */
35 | public setModel(oModel: sap.ui.model.Model, sName?: string|undefined): sap.ui.core.mvc.View {
36 | let view = this.getView();
37 | view.setModel(oModel, sName);
38 | return view;
39 | }
40 |
41 | /**
42 | * Convenience method for getting the resource bundle.
43 | * @public
44 | * @returns {sap.ui.model.resource.ResourceModel} the resourceModel of the component
45 | */
46 | public getResourceBundle(): typeof jQuery.sap.util.ResourceBundle {
47 | let resourceModel = this.getOwnerComponent().getModel("i18n");
48 | //TODO: decide what to do when a method return T|Promise
49 | return resourceModel.getResourceBundle();
50 | }
51 |
52 | /**
53 | * Convenience method for getting the typed owner component.
54 | * @public
55 | * @override
56 | * @returns {typescript.example.ui5app.Component} the owner component
57 | */
58 | public getOwnerComponent(): MyUIComponent {
59 | return super.getOwnerComponent();
60 | }
61 |
62 | /**
63 | * Convenience method for getting an typed element by Id.
64 | * @public
65 | * @override
66 | * @returns the element
67 | */
68 | public byId(sId: string): T {
69 | return super.byId(sId);
70 | }
71 |
72 | /**
73 | * Event handler for navigating back.
74 | * It there is a history entry we go one step back in the browser history
75 | * If not, it will replace the current entry of the browser history with the master route.
76 | * @public
77 | */
78 | public onNavBack(): void {
79 | var sPreviousHash = History.getInstance().getPreviousHash();
80 |
81 | //TODO|ui5ts: History's getPreviousHash() method should return string|undefined instead of just string, like is said on the docs:
82 | // "gets the previous hash in the history - if the last direction was Unknown or there was no navigation yet, undefined will be returned"
83 |
84 | if (sPreviousHash !== undefined) {
85 | history.go(-1);
86 | } else {
87 | this.getRouter().navTo("master", {}, true);
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/test/integration/pages/NotFound.js:
--------------------------------------------------------------------------------
1 | sap.ui.define([
2 | "sap/ui/test/Opa5",
3 | "sap/ui/test/actions/Press",
4 | "sap/ui/test/matchers/PropertyStrictEquals",
5 | "typescript/example/ui5app/test/integration/pages/Common"
6 | ], function(Opa5, Press, PropertyStrictEquals, Common) {
7 | "use strict";
8 |
9 | var sNotFoundPageId = "page",
10 | sNotFoundView = "NotFound",
11 | sDetailNotFoundView = "DetailObjectNotFound";
12 |
13 | Opa5.createPageObjects({
14 | onTheNotFoundPage : {
15 | baseClass : Common,
16 |
17 | actions : {
18 |
19 | iPressTheBackButton : function (sViewName) {
20 | return this.waitFor({
21 | viewName : sViewName,
22 | controlType : "sap.m.Button",
23 | matchers: new PropertyStrictEquals({name : "type", value : "Back"}),
24 | actions : new Press(),
25 | errorMessage : "Did not find the back button"
26 | });
27 | }
28 |
29 | },
30 |
31 | assertions : {
32 |
33 | iShouldSeeTheNotFoundGeneralPage : function (sPageId, sPageViewName) {
34 | return this.waitFor({
35 | controlType : "sap.m.MessagePage",
36 | viewName : sPageViewName,
37 | success : function () {
38 | Opa5.assert.ok(true, "Shows the message page");
39 | },
40 | errorMessage : "Did not reach the empty page"
41 | });
42 | },
43 |
44 | iShouldSeeTheNotFoundPage : function () {
45 | return this.iShouldSeeTheNotFoundGeneralPage(sNotFoundPageId, sNotFoundView);
46 | },
47 |
48 | iShouldSeeTheObjectNotFoundPage : function () {
49 | return this.iShouldSeeTheNotFoundGeneralPage(sNotFoundPageId, sDetailNotFoundView);
50 | },
51 |
52 | theNotFoundPageShouldSayResourceNotFound : function () {
53 | return this.waitFor({
54 | id : sNotFoundPageId,
55 | viewName : sNotFoundView,
56 | success : function (oPage) {
57 | Opa5.assert.strictEqual(oPage.getTitle(), oPage.getModel("i18n").getProperty("notFoundTitle"), "The not found text is shown as title");
58 | Opa5.assert.strictEqual(oPage.getText(), oPage.getModel("i18n").getProperty("notFoundText"), "The resource not found text is shown");
59 | },
60 | errorMessage : "Did not display the resource not found text"
61 | });
62 | },
63 |
64 | theNotFoundPageShouldSayObjectNotFound : function () {
65 | return this.waitFor({
66 | id : sNotFoundPageId,
67 | viewName : sDetailNotFoundView,
68 | success : function (oPage) {
69 | Opa5.assert.strictEqual(oPage.getTitle(), oPage.getModel("i18n").getProperty("detailTitle"), "The object text is shown as title");
70 | Opa5.assert.strictEqual(oPage.getText(), oPage.getModel("i18n").getProperty("noObjectFoundText"), "The object not found text is shown");
71 | },
72 | errorMessage : "Did not display the object not found text"
73 | });
74 | }
75 |
76 | }
77 |
78 | }
79 |
80 | });
81 |
82 | });
--------------------------------------------------------------------------------
/src/view/Detail.view.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
14 |
15 |
16 |
25 |
28 |
29 |
33 |
34 |
38 |
39 |
40 |
41 |
49 |
50 |
51 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
73 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
89 |
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/src/i18n/i18n.properties:
--------------------------------------------------------------------------------
1 | # This is the resource bundle for Master-Detail
2 |
3 | #XTIT: Application name
4 | appTitle=Master-Detail
5 |
6 | #YDES: Application description
7 | appDescription=Best-practice starting point for a master-detail app (standalone)
8 |
9 | #~~~ Master View ~~~~~~~~~~~~~~~~~~~~~~~~~~
10 |
11 | #XTIT: Master view title with placeholder for the number of items
12 | masterTitleCount= ({0})
13 |
14 | #XTOL: Tooltip for the search field
15 | masterSearchTooltip=Enter an name or a part of it.
16 |
17 | #XBLI: text for a list with no data
18 | masterListNoDataText=No are currently available
19 |
20 | #XBLI: text for a list with no data with filter or search
21 | masterListNoDataWithFilterOrSearchText=No matching found
22 |
23 | #XSEL: Option to sort the master list by Name
24 | masterSort1=Sort By
25 |
26 | #XSEL: Option to sort the master list by UnitNumber
27 | masterSort2=Sort By
28 |
29 | #XSEL: Option to filter the master list by UnitNumber
30 | masterFilterName=
31 |
32 | #XSEL: Option to not filter the master list
33 | masterFilterNone=none
34 |
35 | #XSEL: Option to filter the master list by UnitOfMeasure if the value is less than 100
36 | masterFilter1=<100
37 |
38 | #XSEL: Option to filter the master list by UnitOfMeasure if the value is greater than 100
39 | masterFilter2=>100
40 |
41 | #YMSG: Filter text that is displayed above the master list
42 | masterFilterBarText=Filtered by {0}
43 |
44 | #XSEL: Option to not group the master list
45 | masterGroupNone=(Not grouped)
46 |
47 | #XSEL: Option to group the master list by UnitNumber
48 | masterGroup1= Group
49 |
50 | #XGRP: Group header UnitNumber
51 | masterGroup1Header1= 20 or less
52 |
53 | #XGRP: Group header UnitNumber
54 | masterGroup1Header2= higher than 20
55 |
56 | #~~~ Detail View ~~~~~~~~~~~~~~~~~~~~~~~~~~
57 |
58 | #XTIT: Detail view title
59 | detailTitle=
60 |
61 | #XTOL: Icon Tab Bar Info
62 | detailIconTabBarInfo=Info
63 |
64 | #XTOL: Icon Tab Bar Attachments
65 | detailIconTabBarAttachments=Attachments
66 |
67 | #XBLI: Text for the LineItems table with no data
68 | detailLineItemTableNoDataText=No
69 |
70 | #XTIT: Title of the LineItems table
71 | detailLineItemTableHeading=
72 |
73 | #XTIT: Title of the LineItems table
74 | detailLineItemTableHeadingCount= ({0})
75 |
76 | #XGRP: Title for the Name column in the LineItems table
77 | detailLineItemTableIDColumn=
78 |
79 | #XGRP: Title for the UnitNumber column in the LineItems table
80 | detailLineItemTableUnitNumberColumn=
81 |
82 | #XTIT: Send E-Mail subject
83 | shareSendEmailObjectSubject= {0}
84 |
85 | #YMSG: Send E-Mail message
86 | shareSendEmailObjectMessage= {0} (id: {1})\r\n{2}
87 |
88 | #~~~ Not Found View ~~~~~~~~~~~~~~~~~~~~~~~
89 |
90 | #XTIT: Not found view title
91 | notFoundTitle=Not Found
92 |
93 | #YMSG: The Objects not found text is displayed when there is no Objects with this id
94 | noObjectFoundText=This is not available
95 |
96 | #YMSG: The Objects not available text is displayed when there is no data when starting the app
97 | noObjectsAvailableText=No are currently available
98 |
99 | #YMSG: The not found text is displayed when there was an error loading the resource (404 error)
100 | notFoundText=The requested resource was not found
101 |
102 | #~~~ Not Available View ~~~~~~~~~~~~~~~~~~~~~~~
103 |
104 | #XTIT: Master view title
105 | notAvailableViewTitle=
106 |
107 | #~~~ Error Handling ~~~~~~~~~~~~~~~~~~~~~~~
108 |
109 | #YMSG: Error dialog description
110 | errorText=Sorry, a technical error occurred! Please try again later.
--------------------------------------------------------------------------------
/src/view/Master.view.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
13 |
14 |
25 |
26 |
27 |
28 |
32 |
33 |
50 |
51 |
56 |
59 |
60 |
61 |
62 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
81 |
85 |
89 |
90 |
91 |
92 |
93 |
96 |
97 |
98 |
99 |
103 |
107 |
111 |
112 |
113 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "_version": "1.4.0",
3 |
4 | "sap.app": {
5 | "id": "typescript.example.ui5app",
6 | "type": "application",
7 | "i18n": "i18n/i18n.properties",
8 | "title": "{{appTitle}}",
9 | "description": "{{appDescription}}",
10 |
11 | "applicationVersion": {
12 | "version": "1.0.0"
13 | },
14 | "dataSources": {
15 | "mainService": {
16 | "uri": "/here/goes/your/serviceUrl/",
17 | "type": "OData",
18 | "settings": {
19 | "odataVersion": "2.0",
20 | "localUri": "localService/metadata.xml"
21 | }
22 | }
23 | }
24 | },
25 |
26 | "sap.ui": {
27 | "technology": "UI5",
28 | "icons": {
29 | "icon": "sap-icon://detail-view",
30 | "favIcon": "",
31 | "phone": "",
32 | "phone@2": "",
33 | "tablet": "",
34 | "tablet@2": ""
35 | },
36 | "deviceTypes": {
37 | "desktop": true,
38 | "tablet": true,
39 | "phone": true
40 | },
41 | "supportedThemes": [
42 | "sap_hcb",
43 | "sap_belize"
44 | ]
45 | },
46 |
47 | "sap.ui5": {
48 | "rootView": {
49 | "viewName": "typescript.example.ui5app.view.App",
50 | "type": "XML",
51 | "id": "app"
52 | },
53 |
54 | "dependencies": {
55 | "minUI5Version": "1.42.0",
56 | "libs": {
57 | "sap.ui.core": {
58 | "minVersion": "1.42.0"
59 | },
60 | "sap.m": {
61 | "minVersion": "1.42.0"
62 | }
63 | }
64 | },
65 |
66 | "contentDensities": {
67 | "compact": true,
68 | "cozy": true
69 | },
70 |
71 | "models": {
72 | "i18n": {
73 | "type": "sap.ui.model.resource.ResourceModel",
74 | "settings": {
75 | "bundleName": "typescript.example.ui5app.i18n.i18n"
76 | }
77 | },
78 | "": {
79 | "dataSource": "mainService",
80 | "preload": true
81 | }
82 | },
83 |
84 | "routing": {
85 | "config": {
86 | "routerClass": "sap.m.routing.Router",
87 | "viewType": "XML",
88 | "viewPath": "typescript.example.ui5app.view",
89 | "controlId": "idAppControl",
90 | "controlAggregation": "detailPages",
91 | "bypassed": {
92 | "target": [
93 | "master",
94 | "notFound"
95 | ]
96 | },
97 | "async": true
98 | },
99 |
100 | "routes": [
101 | {
102 | "pattern": "",
103 | "name": "master",
104 | "target": [
105 | "object",
106 | "master"
107 | ]
108 | },
109 | {
110 | "pattern": "Objects/{objectId}",
111 | "name": "object",
112 | "target": [
113 | "master",
114 | "object"
115 | ]
116 | }
117 | ],
118 |
119 | "targets": {
120 | "master": {
121 | "viewName": "Master",
122 | "viewLevel": 1,
123 | "viewId": "master",
124 | "controlAggregation": "masterPages"
125 | },
126 | "object": {
127 | "viewName": "Detail",
128 | "viewId": "detail",
129 | "viewLevel": 2
130 | },
131 | "detailObjectNotFound": {
132 | "viewName": "DetailObjectNotFound",
133 | "viewId": "detailObjectNotFound"
134 | },
135 | "detailNoObjectsAvailable": {
136 | "viewName": "DetailNoObjectsAvailable",
137 | "viewId": "detailNoObjectsAvailable"
138 | },
139 | "notFound": {
140 | "viewName": "NotFound",
141 | "viewId": "notFound"
142 | }
143 | }
144 | }
145 | }
146 | }
--------------------------------------------------------------------------------
/src/localService/mockdata/Objects.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "ObjectID" : "ObjectID_1",
4 | "Name" : "Object 1",
5 | "Attribute1" : "Attribute A",
6 | "Attribute2" : "Attribute B",
7 | "UnitOfMeasure" : "UoM",
8 | "UnitNumber" : 81
9 | }, {
10 | "ObjectID" : "ObjectID_2",
11 | "Name" : "Object 2",
12 | "Attribute1" : "Attribute C",
13 | "Attribute2" : "Attribute D",
14 | "UnitOfMeasure" : "UoM",
15 | "UnitNumber" : 42
16 | }, {
17 | "ObjectID" : "ObjectID_3",
18 | "Name" : "Object 3",
19 | "Attribute1" : "Attribute E",
20 | "Attribute2" : "Attribute F",
21 | "UnitOfMeasure" : "UoM",
22 | "UnitNumber" : 27
23 | }, {
24 | "ObjectID" : "ObjectID_4",
25 | "Name" : "Object 4",
26 | "Attribute1" : "Attribute G",
27 | "Attribute2" : "Attribute H",
28 | "UnitOfMeasure" : "UoM",
29 | "UnitNumber" : 13
30 | }, {
31 | "ObjectID" : "ObjectID_5",
32 | "Name" : "Object 5",
33 | "Attribute1" : "Attribute I",
34 | "Attribute2" : "Attribute J",
35 | "UnitOfMeasure" : "UoM",
36 | "UnitNumber" : 121
37 | }, {
38 | "ObjectID" : "ObjectID_6",
39 | "Name" : "Object 6",
40 | "Attribute1" : "Attribute K",
41 | "Attribute2" : "Attribute L",
42 | "UnitOfMeasure" : "UoM",
43 | "UnitNumber" : 481
44 | }, {
45 | "ObjectID" : "ObjectID_7",
46 | "Name" : "Object 7",
47 | "Attribute1" : "Attribute M",
48 | "Attribute2" : "Attribute N",
49 | "UnitOfMeasure" : "UoM",
50 | "UnitNumber" : 63
51 | }, {
52 | "ObjectID" : "ObjectID_8",
53 | "Name" : "Object 8",
54 | "Attribute1" : "Attribute O",
55 | "Attribute2" : "Attribute P",
56 | "UnitOfMeasure" : "UoM",
57 | "UnitNumber" : 28
58 | }, {
59 | "ObjectID" : "ObjectID_9",
60 | "Name" : "Object 9",
61 | "Attribute1" : "Attribute Q",
62 | "Attribute2" : "Attribute R",
63 | "UnitOfMeasure" : "UoM",
64 | "UnitNumber" : 25
65 | }, {
66 | "ObjectID" : "ObjectID_10",
67 | "Name" : "Object 10",
68 | "Attribute1" : "Attribute S",
69 | "Attribute2" : "Attribute T",
70 | "UnitOfMeasure" : "UoM",
71 | "UnitNumber" : 106
72 | }, {
73 | "ObjectID" : "ObjectID_11",
74 | "Name" : "Object 11",
75 | "Attribute1" : "Attribute U",
76 | "Attribute2" : "Attribute V",
77 | "UnitOfMeasure" : "UoM",
78 | "UnitNumber" : 61
79 | }, {
80 | "ObjectID" : "ObjectID_12",
81 | "Name" : "Object 12",
82 | "Attribute1" : "Attribute W",
83 | "Attribute2" : "Attribute X",
84 | "UnitOfMeasure" : "UoM",
85 | "UnitNumber" : 3006
86 | }, {
87 | "ObjectID" : "ObjectID_13",
88 | "Name" : "Object 13",
89 | "Attribute1" : "Attribute Y",
90 | "Attribute2" : "Attribute Z",
91 | "UnitOfMeasure" : "UoM",
92 | "UnitNumber" : 256
93 | }, {
94 | "ObjectID" : "ObjectID_14",
95 | "Name" : "Object 14",
96 | "Attribute1" : "Attribute AA",
97 | "Attribute2" : "Attribute AB",
98 | "UnitOfMeasure" : "UoM",
99 | "UnitNumber" : 234
100 | }, {
101 | "ObjectID" : "ObjectID_15",
102 | "Name" : "Object 15",
103 | "Attribute1" : "Attribute AC",
104 | "Attribute2" : "Attribute AD",
105 | "UnitOfMeasure" : "UoM",
106 | "UnitNumber" : 34
107 | }, {
108 | "ObjectID" : "ObjectID_16",
109 | "Name" : "Object 16",
110 | "Attribute1" : "Attribute AE",
111 | "Attribute2" : "Attribute AF",
112 | "UnitOfMeasure" : "UoM",
113 | "UnitNumber" : -60
114 | }, {
115 | "ObjectID" : "ObjectID_17",
116 | "Name" : "Object 17",
117 | "Attribute1" : "Attribute AG",
118 | "Attribute2" : "Attribute AH",
119 | "UnitOfMeasure" : "UoM",
120 | "UnitNumber" : 3006
121 | }, {
122 | "ObjectID" : "ObjectID_18",
123 | "Name" : "Object 18",
124 | "Attribute1" : "Attribute AI",
125 | "Attribute2" : "Attribute AJ",
126 | "UnitOfMeasure" : "UoM",
127 | "UnitNumber" : 4587
128 | }, {
129 | "ObjectID" : "ObjectID_19",
130 | "Name" : "Object 19",
131 | "Attribute1" : "Attribute AK",
132 | "Attribute2" : "Attribute AL",
133 | "UnitOfMeasure" : "UoM",
134 | "UnitNumber" : 560
135 | }, {
136 | "ObjectID" : "ObjectID_20",
137 | "Name" : "Object 20",
138 | "Attribute1" : "Attribute AM",
139 | "Attribute2" : "Attribute AN",
140 | "UnitOfMeasure" : "UoM",
141 | "UnitNumber" : 4503
142 | }, {
143 | "ObjectID" : "ObjectID_21",
144 | "Name" : "Object 21",
145 | "Attribute1" : "Attribute AO",
146 | "Attribute2" : "Attribute AP",
147 | "UnitOfMeasure" : "UoM",
148 | "UnitNumber" : 403
149 | }
150 | ]
--------------------------------------------------------------------------------
/src/test/integration/MasterJourney.js:
--------------------------------------------------------------------------------
1 | /*global QUnit*/
2 |
3 | sap.ui.define([
4 | "sap/ui/test/opaQunit"
5 | ], function (opaTest) {
6 | "use strict";
7 |
8 | QUnit.module("Master List");
9 |
10 | opaTest("Should see the master list with all entries", function (Given, When, Then) {
11 | // Arrangements
12 | Given.iStartTheApp();
13 |
14 | //Actions
15 | When.onTheMasterPage.iLookAtTheScreen();
16 |
17 | // Assertions
18 | Then.onTheMasterPage.iShouldSeeTheList().
19 | and.theListShouldHaveAllEntries().
20 | and.theHeaderShouldDisplayAllEntries().
21 | and.theListShouldContainOnlyFormattedUnitNumbers();
22 | });
23 |
24 | opaTest("Search for the First object should deliver results that contain the firstObject in the name", function (Given, When, Then) {
25 | //Actions
26 | When.onTheMasterPage.iSearchForTheFirstObject();
27 |
28 | // Assertions
29 | Then.onTheMasterPage.theListShowsOnlyObjectsWithTheSearchStringInTheirTitle();
30 | });
31 |
32 | opaTest("Entering something that cannot be found into search field and pressing search field's refresh should leave the list as it was", function (Given, When, Then) {
33 | //Actions
34 | When.onTheMasterPage.iTypeSomethingInTheSearchThatCannotBeFoundAndTriggerRefresh();
35 |
36 | // Assertions
37 | Then.onTheMasterPage.theListHasEntries();
38 | });
39 |
40 | opaTest("Entering something that cannot be found into search field and pressing 'search' should display the list's 'not found' message", function (Given, When, Then) {
41 | //Actions
42 | When.onTheMasterPage.iSearchForSomethingWithNoResults();
43 |
44 | // Assertions
45 | Then.onTheMasterPage.iShouldSeeTheNoDataTextForNoSearchResults().
46 | and.theListHeaderDisplaysZeroHits();
47 | });
48 |
49 | opaTest("Should display items again if the searchfield is emptied", function (Given, When, Then) {
50 | //Actions
51 | When.onTheMasterPage.iClearTheSearch();
52 |
53 | // Assertions
54 | Then.onTheMasterPage.theListShouldHaveAllEntries();
55 | });
56 |
57 | opaTest("MasterList Sorting on UnitNumber", function(Given, When, Then) {
58 | // Actions
59 | When.onTheMasterPage.iSortTheListOnUnitNumber();
60 |
61 | // Assertions
62 | Then.onTheMasterPage.theListShouldBeSortedAscendingOnUnitNumber();
63 | });
64 |
65 | opaTest("MasterList Sorting on Name", function(Given, When, Then) {
66 | // Actions
67 | When.onTheMasterPage.iSortTheListOnName();
68 |
69 | // Assertions
70 | Then.onTheMasterPage.theListShouldBeSortedAscendingOnName();
71 | });
72 |
73 | opaTest("MasterList Filtering on UnitNumber less than 100", function(Given, When, Then) {
74 |
75 | // Action
76 | When.onTheMasterPage.iOpenViewSettingsDialog().
77 | and.iSelectListItemInViewSettingsDialog("").
78 | and.iSelectListItemInViewSettingsDialog("<100 ").
79 | and.iPressOKInViewSelectionDialog();
80 |
81 | // Assertion
82 | Then.onTheMasterPage.theMasterListShouldBeFilteredOnUnitNumberValueLessThanTheGroupBoundary();
83 | });
84 |
85 | opaTest("MasterList Filtering on UnitNumber more than 100", function(Given, When, Then) {
86 | // Action
87 | When.onTheMasterPage.iOpenViewSettingsDialog().
88 | and.iSelectListItemInViewSettingsDialog(">100 ").
89 | and.iPressOKInViewSelectionDialog();
90 |
91 | // Assertion
92 | Then.onTheMasterPage.theMasterListShouldBeFilteredOnUnitNumberValueMoreThanTheGroupBoundary();
93 | });
94 |
95 | opaTest("MasterList remove filter should display all items", function(Given, When, Then) {
96 | // Action
97 | When.onTheMasterPage.iOpenViewSettingsDialog().
98 | and.iPressResetInViewSelectionDialog().
99 | and.iPressOKInViewSelectionDialog();
100 |
101 | // Assertion
102 | Then.onTheMasterPage.theListShouldHaveAllEntries();
103 | });
104 |
105 | opaTest("MasterList grouping created group headers", function(Given, When, Then) {
106 | // Action
107 | When.onTheMasterPage.iGroupTheList();
108 |
109 | // Assertion
110 | Then.onTheMasterPage.theListShouldContainAGroupHeader();
111 | });
112 |
113 | opaTest("Remove grouping from MasterList delivers initial list", function(Given, When, Then) {
114 | // Action
115 | When.onTheMasterPage.iRemoveListGrouping();
116 |
117 | // Assertion
118 | Then.onTheMasterPage.theListShouldNotContainGroupHeaders().
119 | and.theListShouldHaveAllEntries();
120 | });
121 |
122 | opaTest("Grouping the master list and filtering it by the object identifier should deliver the initial list", function(Given, When, Then) {
123 | // Action
124 | When.onTheMasterPage.iGroupTheList().
125 | and.iSortTheListOnName();
126 |
127 | // Assertion
128 | Then.onTheMasterPage.theListShouldNotContainGroupHeaders().
129 | and.iTeardownMyAppFrame();
130 | });
131 |
132 | });
--------------------------------------------------------------------------------
/src/localService/metadata.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
8 |
9 |
11 |
12 |
13 |
14 |
17 |
20 |
23 |
26 |
29 |
32 |
33 |
34 |
35 |
37 |
38 |
39 |
40 |
43 |
46 |
49 |
52 |
55 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
76 |
80 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/src/test/unit/controller/ListSelector.js:
--------------------------------------------------------------------------------
1 | /*global QUnit,sinon*/
2 |
3 | sap.ui.define([
4 | "typescript/example/ui5app/controller/ListSelector",
5 | "sap/ui/thirdparty/sinon",
6 | "sap/ui/thirdparty/sinon-qunit"
7 | ], function(ListSelector) {
8 | "use strict";
9 |
10 | QUnit.module("Initialization", {
11 | beforeEach : function () {
12 | sinon.config.useFakeTimers = false;
13 | this.oListSelector = new ListSelector();
14 | },
15 | afterEach : function () {
16 | this.oListSelector.destroy();
17 | }
18 | });
19 |
20 | QUnit.test("Should initialize the List loading promise", function (assert) {
21 | // Arrange
22 | var done = assert.async(),
23 | fnRejectSpy = this.spy(),
24 | fnResolveSpy = this.spy();
25 |
26 | // Act
27 | this.oListSelector.oWhenListLoadingIsDone.then(fnResolveSpy, fnRejectSpy);
28 |
29 | // Assert
30 | setTimeout(function () {
31 | assert.strictEqual(fnResolveSpy.callCount, 0, "Did not resolve the promise");
32 | assert.strictEqual(fnRejectSpy.callCount, 0, "Did not reject the promise");
33 | done();
34 | }, 0);
35 | });
36 |
37 | QUnit.module("List loading", {
38 | beforeEach : function () {
39 | sinon.config.useFakeTimers = false;
40 | this.oListSelector = new ListSelector();
41 | },
42 | afterEach : function () {
43 | this.oListSelector.destroy();
44 | }
45 | });
46 |
47 | function createListStub (bCreateListItem, sBindingPath) {
48 | var fnGetParameter = function () {
49 | return true;
50 | },
51 | oDataStub = {
52 | getParameter : fnGetParameter
53 | },
54 | fnAttachEventOnce = function (sEventName, oData, fnCallback) {
55 | fnCallback(oDataStub);
56 | },
57 | fnGetBinding = this.stub().returns({
58 | attachEventOnce : fnAttachEventOnce
59 | }),
60 | fnAttachEvent = function (sEventName, fnCallback, oContext) {
61 | fnCallback.apply(oContext);
62 | },
63 | oListItemStub = {
64 | getBindingContext : this.stub().returns({
65 | getPath : this.stub().returns(sBindingPath)
66 | })
67 | },
68 | aListItems = [];
69 |
70 | if (bCreateListItem) {
71 | aListItems.push(oListItemStub);
72 | }
73 |
74 | return {
75 | attachEvent : fnAttachEvent,
76 | attachEventOnce : fnAttachEventOnce,
77 | getBinding : fnGetBinding,
78 | getItems : this.stub().returns(aListItems)
79 | };
80 | }
81 |
82 | QUnit.test("Should resolve the list loading promise, if the list has items", function (assert) {
83 | // Arrange
84 | var done = assert.async(),
85 | fnRejectSpy = this.spy(),
86 | fnResolveSpy = function (sBindingPath) {
87 | // Assert
88 | assert.strictEqual(sBindingPath, sBindingPath, "Did pass the binding path");
89 | assert.strictEqual(fnRejectSpy.callCount, 0, "Did not reject the promise");
90 | done();
91 | };
92 |
93 | // Act
94 | this.oListSelector.oWhenListLoadingIsDone.then(fnResolveSpy, fnRejectSpy);
95 | this.oListSelector.setBoundMasterList(createListStub.call(this, true, "anything"));
96 | });
97 |
98 | QUnit.test("Should reject the list loading promise, if the list has no items", function (assert) {
99 | // Arrange
100 | var done = assert.async(),
101 | fnResolveSpy = this.spy(),
102 | fnRejectSpy = function () {
103 | // Assert
104 | assert.strictEqual(fnResolveSpy.callCount, 0, "Did not resolve the promise");
105 | done();
106 | };
107 |
108 | // Act
109 | this.oListSelector.oWhenListLoadingIsDone.then(fnResolveSpy, fnRejectSpy);
110 | this.oListSelector.setBoundMasterList(createListStub.call(this, false));
111 | });
112 |
113 | QUnit.module("Selecting item in the list", {
114 | beforeEach : function () {
115 | sinon.config.useFakeTimers = false;
116 | this.oListSelector = new ListSelector();
117 | this.oListSelector.oWhenListLoadingIsDone = {
118 | then : function (fnAct) {
119 | this.fnAct = fnAct;
120 | }.bind(this)
121 | };
122 | },
123 | afterEach : function () {
124 | this.oListSelector.destroy();
125 | }
126 | });
127 |
128 | function createStubbedListItem (sBindingPath) {
129 | return {
130 | getBindingContext : this.stub().returns({
131 | getPath : this.stub().returns(sBindingPath)
132 | })
133 | };
134 | }
135 |
136 | QUnit.test("Should select an Item of the list when it is loaded and the binding contexts match", function (assert) {
137 | // Arrange
138 | var sBindingPath = "anything",
139 | oListItemToSelect = createStubbedListItem.call(this, sBindingPath),
140 | oSelectedListItemStub = createStubbedListItem.call(this, "a different binding path");
141 |
142 | this.oListSelector._oList = {
143 | getMode : this.stub().returns("SingleSelectMaster"),
144 | getSelectedItem : this.stub().returns(oSelectedListItemStub),
145 | getItems : this.stub().returns([ oSelectedListItemStub, oListItemToSelect, createListStub.call(this, "yet another list binding") ]),
146 | setSelectedItem : function (oItem) {
147 | //Assert
148 | assert.strictEqual(oItem, oListItemToSelect, "Did select the list item with a matching binding context");
149 | }
150 | };
151 |
152 | // Act
153 | this.oListSelector.selectAListItem(sBindingPath);
154 | // Resolve list loading
155 | this.fnAct();
156 | });
157 |
158 | QUnit.test("Should not select an Item of the list when it is already selected", function (assert) {
159 | // Arrange
160 | var sBindingPath = "anything",
161 | oSelectedListItemStub = createStubbedListItem.call(this, sBindingPath);
162 |
163 | this.oListSelector._oList = {
164 | getMode: this.stub().returns("SingleSelectMaster"),
165 | getSelectedItem : this.stub().returns(oSelectedListItemStub)
166 | };
167 |
168 | // Act
169 | this.oListSelector.selectAListItem(sBindingPath);
170 | // Resolve list loading
171 | this.fnAct();
172 |
173 | // Assert
174 | assert.ok(true, "did not fail");
175 | });
176 |
177 | QUnit.test("Should not select an item of the list when the list has the selection mode none", function (assert) {
178 | // Arrange
179 | var sBindingPath = "anything";
180 |
181 | this.oListSelector._oList = {
182 | getMode : this.stub().returns("None")
183 | };
184 |
185 | // Act
186 | this.oListSelector.selectAListItem(sBindingPath);
187 | // Resolve list loading
188 | this.fnAct();
189 |
190 | // Assert
191 | assert.ok(true, "did not fail");
192 | });
193 |
194 | });
--------------------------------------------------------------------------------
/src/controller/ListSelector.ts:
--------------------------------------------------------------------------------
1 | import BaseObject from "sap/ui/base/Object";
2 |
3 | @UI5("typescript.example.ui5app.model.ListSelector")
4 | export default class ListSelector extends BaseObject
5 | {
6 | private _oList: sap.m.List
7 | private _fnResolveListHasBeenSet: (oList: sap.m.List) => void;
8 | private _oWhenListHasBeenSet: Promise;
9 | private oWhenListLoadingIsDone: Promise<{ list: sap.m.List, firstListitem: sap.m.ListItemBase }>;
10 |
11 | /**
12 | * Provides a convenience API for selecting list items. All the functions will wait until the initial load of the a List passed to the instance by the setBoundMasterList
13 | * function.
14 | * @class
15 | * @public
16 | * @alias typescript.example.ui5app.model.ListSelector
17 | */
18 | constructor() {
19 | super();
20 | this._oWhenListHasBeenSet = new Promise((fnResolveListHasBeenSet: (oList: sap.m.List) => void) => {
21 | this._fnResolveListHasBeenSet = fnResolveListHasBeenSet;
22 | });
23 | // This promise needs to be created in the constructor, since it is allowed to
24 | // invoke selectItem functions before calling setBoundMasterList
25 | this.oWhenListLoadingIsDone = new Promise((fnResolve, fnReject) => {
26 | // Used to wait until the setBound masterList function is invoked
27 | this._oWhenListHasBeenSet
28 | .then((oList: sap.m.List) => {
29 | oList.getBinding("items").attachEventOnce("dataReceived",
30 | (oEvent: sap.ui.base.Event) => {
31 | if (!oEvent.getParameter("data")) {
32 | fnReject({
33 | list : oList,
34 | error : true
35 | });
36 | }
37 | var oFirstListItem = oList.getItems()[0];
38 | if (oFirstListItem) {
39 | // Have to make sure that first list Item is selected
40 | // and a select event is triggered. Like that, the corresponding
41 | // detail page is loaded automatically
42 | fnResolve({
43 | list : oList,
44 | firstListitem : oFirstListItem
45 | });
46 | } else {
47 | // No items in the list
48 | fnReject({
49 | list : oList,
50 | error : false
51 | });
52 | }
53 | }
54 | );
55 | });
56 | });
57 | }
58 |
59 | /**
60 | * A bound list should be passed in here. Should be done, before the list has received its initial data from the server.
61 | * May only be invoked once per ListSelector instance.
62 | * @param {sap.m.List} oList The list all the select functions will be invoked on.
63 | * @public
64 | */
65 | public setBoundMasterList(oList: sap.m.List): void {
66 | this._oList = oList;
67 | this._fnResolveListHasBeenSet(oList);
68 | }
69 |
70 |
71 | /**
72 | * Tries to select and scroll to a list item with a matching binding context. If there are no items matching the binding context or the ListMode is none,
73 | * no selection/scrolling will happen
74 | * @param {string} sBindingPath the binding path matching the binding path of a list item
75 | * @public
76 | */
77 | public selectAListItem(sBindingPath: string): void {
78 |
79 | this.oWhenListLoadingIsDone.then(
80 | () => {
81 | var oList = this._oList,
82 | oSelectedItem: sap.m.ListItemBase;
83 |
84 | if (oList.getMode() === sap.m.ListMode.None) {
85 | return;
86 | }
87 |
88 | oSelectedItem = oList.getSelectedItem();
89 |
90 | //TODO|openui5: getPath's argument must be optional
91 | // skip update if the current selection is already matching the object path
92 | if (oSelectedItem && oSelectedItem.getBindingContext().getPath("") === sBindingPath) {
93 | return;
94 | }
95 |
96 | oList.getItems().some((oItem: sap.m.ListItemBase) => {
97 | //TODO|openui5: getPath's argument must be optional
98 | if (oItem.getBindingContext() && oItem.getBindingContext().getPath("") === sBindingPath) {
99 | oList.setSelectedItem(oItem, true);
100 | return true;
101 | }
102 | return false;
103 | });
104 | },
105 | () => jQuery.sap.log.warning("Could not select the list item with the path" + sBindingPath + " because the list encountered an error or had no items")
106 | );
107 | }
108 |
109 |
110 | /* =========================================================== */
111 | /* Convenience Functions for List Selection Change Event */
112 | /* =========================================================== */
113 |
114 | /**
115 | * Attaches a listener and listener function to the ListSelector's bound master list. By using
116 | * a promise, the listener is added, even if the list is not available when 'attachListSelectionChange'
117 | * is called.
118 | * @param {function} fnFunction the function to be executed when the list fires a selection change event
119 | * @param {function} oListener the listener object
120 | * @return {typescript.example.ui5app.model.ListSelector} the list selector object for method chaining
121 | * @public
122 | */
123 | public attachListSelectionChange(fnFunction: Function, oListener: any): ListSelector {
124 | this._oWhenListHasBeenSet.then(() => {
125 | this._oList.attachSelectionChange(fnFunction, oListener);
126 | });
127 | return this;
128 | }
129 |
130 | /**
131 | * Detaches a listener and listener function from the ListSelector's bound master list. By using
132 | * a promise, the listener is removed, even if the list is not available when 'detachListSelectionChange'
133 | * is called.
134 | * @param {function} fnFunction the function to be executed when the list fires a selection change event
135 | * @param {function} oListener the listener object
136 | * @return {typescript.example.ui5app.model.ListSelector} the list selector object for method chaining
137 | * @public
138 | */
139 | public detachListSelectionChange(fnFunction: Function, oListener: any): ListSelector {
140 | this._oWhenListHasBeenSet.then(() => {
141 | this._oList.detachSelectionChange(fnFunction, oListener);
142 | });
143 | return this;
144 | }
145 |
146 | /**
147 | * Removes all selections from master list.
148 | * Does not trigger 'selectionChange' event on master list, though.
149 | * @public
150 | */
151 | public clearMasterListSelection(): void {
152 | //use promise to make sure that 'this._oList' is available
153 | this._oWhenListHasBeenSet.then(() => {
154 | this._oList.removeSelections(true);
155 | });
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/src/controller/Detail.controller.ts:
--------------------------------------------------------------------------------
1 | import BaseController from "typescript/example/ui5app/controller/BaseController";
2 | import JSONModel from "sap/ui/model/json/JSONModel";
3 | import formatter from "typescript/example/ui5app/model/formatter";
4 |
5 | @UI5("typescript.example.ui5app.controller.Detail")
6 | export default class Detail extends BaseController
7 | {
8 | public formatter = formatter;
9 |
10 | /* =========================================================== */
11 | /* lifecycle methods */
12 | /* =========================================================== */
13 |
14 | onInit() {
15 | // Model used to manipulate control states. The chosen values make sure,
16 | // detail page is busy indication immediately so there is no break in
17 | // between the busy indication for loading the view's meta data
18 | var oViewModel = new JSONModel({
19 | busy : false,
20 | delay : 0,
21 | lineItemListTitle : this.getResourceBundle().getText("detailLineItemTableHeading")
22 | });
23 |
24 | this.getRouter().getRoute("object").attachPatternMatched(this._onObjectMatched, this);
25 |
26 | this.setModel(oViewModel, "detailView");
27 |
28 | this.getOwnerComponent().getModel().metadataLoaded().then(this._onMetadataLoaded.bind(this));
29 | }
30 |
31 | /* =========================================================== */
32 | /* event handlers */
33 | /* =========================================================== */
34 |
35 | /**
36 | * Event handler when the share by E-Mail button has been clicked
37 | * @public
38 | */
39 | onShareEmailPress() {
40 | var oViewModel = this.getModel("detailView");
41 |
42 | sap.m.URLHelper.triggerEmail(
43 | null,
44 | oViewModel.getProperty("/shareSendEmailSubject"),
45 | oViewModel.getProperty("/shareSendEmailMessage")
46 | );
47 | }
48 |
49 |
50 | /**
51 | * Updates the item count within the line item table's header
52 | * @param {object} oEvent an event containing the total number of items in the list
53 | * @private
54 | */
55 | onListUpdateFinished(oEvent: sap.ui.base.Event) {
56 | var sTitle,
57 | iTotalItems = oEvent.getParameter("total"),
58 | oViewModel = this.getModel("detailView");
59 |
60 | // only update the counter if the length is final
61 | if ((this.byId("lineItemsList").getBinding("items")).isLengthFinal()) {
62 | if (iTotalItems) {
63 | sTitle = this.getResourceBundle().getText("detailLineItemTableHeadingCount", [iTotalItems]);
64 | } else {
65 | //Display 'Line Items' instead of 'Line items (0)'
66 | sTitle = this.getResourceBundle().getText("detailLineItemTableHeading");
67 | }
68 | oViewModel.setProperty("/lineItemListTitle", sTitle);
69 | }
70 | }
71 |
72 | /* =========================================================== */
73 | /* begin: internal methods */
74 | /* =========================================================== */
75 |
76 | /**
77 | * Binds the view to the object path and expands the aggregated line items.
78 | * @function
79 | * @param {sap.ui.base.Event} oEvent pattern match event in route 'object'
80 | * @private
81 | */
82 | private _onObjectMatched(oEvent: sap.ui.base.Event): void {
83 | var sObjectId = oEvent.getParameter("arguments").objectId;
84 | this.getModel().metadataLoaded().then(() => {
85 | var sObjectPath = this.getModel().createKey("Objects", {
86 | ObjectID : sObjectId
87 | });
88 | this._bindView("/" + sObjectPath);
89 | });
90 | }
91 |
92 | /**
93 | * Binds the view to the object path. Makes sure that detail view displays
94 | * a busy indicator while data for the corresponding element binding is loaded.
95 | * @function
96 | * @param {string} sObjectPath path to the object to be bound to the view.
97 | * @private
98 | */
99 | private _bindView(sObjectPath: string): void {
100 | // Set busy indicator during view binding
101 | var oViewModel = this.getModel("detailView");
102 |
103 | // If the view was not bound yet its not busy, only if the binding requests data it is set to busy again
104 | oViewModel.setProperty("/busy", false);
105 |
106 | this.getView().bindElement({
107 | path : sObjectPath,
108 | events: {
109 | change : this._onBindingChange.bind(this),
110 | dataRequested : function () {
111 | oViewModel.setProperty("/busy", true);
112 | },
113 | dataReceived: function () {
114 | oViewModel.setProperty("/busy", false);
115 | }
116 | }
117 | });
118 | }
119 |
120 | private _onBindingChange(): void {
121 | var oView = this.getView(),
122 | oElementBinding = oView.getElementBinding(undefined);
123 |
124 | // No data for the binding
125 | //TODO|openui5: method getBoundContext() doesn't exists on the docs, but it do exists on the ui5 library code.
126 | if (!(oElementBinding).getBoundContext()) {
127 | this.getRouter().getTargets().display("detailObjectNotFound");
128 | // if object could not be found, the selection in the master list
129 | // does not make sense anymore.
130 | this.getOwnerComponent().oListSelector.clearMasterListSelection();
131 | return;
132 | }
133 |
134 | //TODO|openui5: method getPath() doesn't exists on the docs, but it do exists on the ui5 library code.
135 | var sPath: string = (oElementBinding).getPath(),
136 | oResourceBundle = this.getResourceBundle(),
137 | oObject = (oView.getModel(undefined)).getObject(sPath),
138 | sObjectId = oObject.ObjectID,
139 | sObjectName = oObject.Name,
140 | oViewModel = this.getModel("detailView");
141 |
142 | this.getOwnerComponent().oListSelector.selectAListItem(sPath);
143 |
144 | oViewModel.setProperty("/shareSendEmailSubject",
145 | oResourceBundle.getText("shareSendEmailObjectSubject", [sObjectId]));
146 | oViewModel.setProperty("/shareSendEmailMessage",
147 | oResourceBundle.getText("shareSendEmailObjectMessage", [sObjectName, sObjectId, location.href]));
148 | }
149 |
150 | private _onMetadataLoaded(): void {
151 | // Store original busy indicator delay for the detail view
152 | var iOriginalViewBusyDelay = this.getView().getBusyIndicatorDelay(),
153 | oViewModel = this.getModel("detailView"),
154 | oLineItemTable = this.byId("lineItemsList"),
155 | iOriginalLineItemTableBusyDelay = oLineItemTable.getBusyIndicatorDelay();
156 |
157 | // Make sure busy indicator is displayed immediately when
158 | // detail view is displayed for the first time
159 | oViewModel.setProperty("/delay", 0);
160 | oViewModel.setProperty("/lineItemTableDelay", 0);
161 |
162 | oLineItemTable.attachEventOnce("updateFinished", undefined, () => {
163 | // Restore original busy indicator delay for line item table
164 | oViewModel.setProperty("/lineItemTableDelay", iOriginalLineItemTableBusyDelay);
165 | });
166 |
167 | // Binding the view will set it to not busy - so the view is always busy if it is not bound
168 | oViewModel.setProperty("/busy", true);
169 | // Restore original busy indicator delay for the detail view
170 | oViewModel.setProperty("/delay", iOriginalViewBusyDelay);
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/src/test/integration/pages/Detail.js:
--------------------------------------------------------------------------------
1 | sap.ui.define([
2 | "sap/ui/test/Opa5",
3 | "sap/ui/test/actions/Press",
4 | "typescript/example/ui5app/test/integration/pages/Common",
5 | "sap/ui/test/matchers/AggregationLengthEquals",
6 | "sap/ui/test/matchers/AggregationFilled",
7 | "sap/ui/test/matchers/PropertyStrictEquals"
8 | ], function(Opa5, Press, Common, AggregationLengthEquals, AggregationFilled, PropertyStrictEquals) {
9 | "use strict";
10 |
11 | var sViewName = "Detail";
12 |
13 | Opa5.createPageObjects({
14 | onTheDetailPage : {
15 | baseClass : Common,
16 |
17 | actions : {
18 |
19 | iPressTheBackButton : function () {
20 | return this.waitFor({
21 | id : "page",
22 | viewName : sViewName,
23 | actions: new Press(),
24 | errorMessage : "Did not find the nav button on detail page"
25 | });
26 | }
27 |
28 | },
29 |
30 | assertions : {
31 |
32 | iShouldSeeTheBusyIndicator : function () {
33 | return this.waitFor({
34 | id : "page",
35 | viewName : sViewName,
36 | success : function (oPage) {
37 | // we set the view busy, so we need to query the parent of the app
38 | Opa5.assert.ok(oPage.getBusy(), "The detail view is busy");
39 | },
40 | errorMessage : "The detail view is not busy."
41 | });
42 | },
43 |
44 | iShouldSeeNoBusyIndicator : function () {
45 | return this.waitFor({
46 | id : "page",
47 | viewName : sViewName,
48 | matchers : function (oPage) {
49 | return !oPage.getBusy();
50 | },
51 | success : function (oPage) {
52 | // we set the view busy, so we need to query the parent of the app
53 | Opa5.assert.ok(!oPage.getBusy(), "The detail view is not busy");
54 | },
55 | errorMessage : "The detail view is busy."
56 | });
57 | },
58 |
59 | theObjectPageShowsTheFirstObject : function () {
60 | return this.iShouldBeOnTheObjectNPage(0);
61 | },
62 |
63 | iShouldBeOnTheObjectNPage : function (iObjIndex) {
64 | return this.waitFor(this.createAWaitForAnEntitySet({
65 | entitySet : "Objects",
66 | success : function (aEntitySet) {
67 | var sItemName = aEntitySet[iObjIndex].Name;
68 |
69 | this.waitFor({
70 | controlType : "sap.m.ObjectHeader",
71 | viewName : sViewName,
72 | matchers : new PropertyStrictEquals({name : "title", value: aEntitySet[iObjIndex].Name}),
73 | success : function () {
74 | Opa5.assert.ok(true, "was on the first object page with the name " + sItemName);
75 | },
76 | errorMessage : "First object is not shown"
77 | });
78 | }
79 | }));
80 | },
81 |
82 | iShouldSeeTheRememberedObject : function () {
83 | return this.waitFor({
84 | success : function () {
85 | var sBindingPath = this.getContext().currentItem.bindingPath;
86 | this._waitForPageBindingPath(sBindingPath);
87 | }
88 | });
89 | },
90 |
91 | _waitForPageBindingPath : function (sBindingPath) {
92 | return this.waitFor({
93 | id : "page",
94 | viewName : sViewName,
95 | matchers : function (oPage) {
96 | return oPage.getBindingContext() && oPage.getBindingContext().getPath() === sBindingPath;
97 | },
98 | success : function (oPage) {
99 | Opa5.assert.strictEqual(oPage.getBindingContext().getPath(), sBindingPath, "was on the remembered detail page");
100 | },
101 | errorMessage : "Remembered object " + sBindingPath + " is not shown"
102 | });
103 | },
104 |
105 | iShouldSeeTheObjectLineItemsList : function () {
106 | return this.waitFor({
107 | id : "lineItemsList",
108 | viewName : sViewName,
109 | success : function (oList) {
110 | Opa5.assert.ok(oList, "Found the line items list.");
111 | }
112 | });
113 | },
114 |
115 | theLineItemsListShouldHaveTheCorrectNumberOfItems : function () {
116 | return this.waitFor(this.createAWaitForAnEntitySet({
117 | entitySet : "LineItems",
118 | success : function (aEntitySet) {
119 |
120 | return this.waitFor({
121 | id : "lineItemsList",
122 | viewName : sViewName,
123 | matchers : new AggregationFilled({name : "items"}),
124 | check: function (oList) {
125 |
126 | var sObjectID = oList.getBindingContext().getProperty("ObjectID");
127 |
128 | var iLength = aEntitySet.filter(function (oLineItem) {
129 | return oLineItem.ObjectID === sObjectID;
130 | }).length;
131 |
132 | return oList.getItems().length === iLength;
133 | },
134 | success : function () {
135 | Opa5.assert.ok(true, "The list has the correct number of items");
136 | },
137 | errorMessage : "The list does not have the correct number of items."
138 | });
139 | }
140 | }));
141 | },
142 |
143 | theDetailViewShouldContainOnlyFormattedUnitNumbers : function () {
144 | return this.theUnitNumbersShouldHaveTwoDecimals("sap.m.ObjectHeader",
145 | sViewName,
146 | "Object header are properly formatted",
147 | "Object view has no entries which can be checked for their formatting");
148 | },
149 |
150 | theLineItemsTableShouldContainOnlyFormattedUnitNumbers : function () {
151 | return this.theUnitNumbersShouldHaveTwoDecimals("sap.m.ObjectNumber",
152 | sViewName,
153 | "Object numbers are properly formatted",
154 | "LineItmes Table has no entries which can be checked for their formatting");
155 | },
156 |
157 | theLineItemsHeaderShouldDisplayTheAmountOfEntries : function () {
158 | return this.waitFor({
159 | id : "lineItemsList",
160 | viewName : sViewName,
161 | matchers : new AggregationFilled({name : "items"}),
162 | success : function (oList) {
163 | var iNumberOfItems = oList.getItems().length;
164 | return this.waitFor({
165 | id : "lineItemsHeader",
166 | viewName : sViewName,
167 | matchers : new PropertyStrictEquals({name: "text", value: " (" + iNumberOfItems + ")"}),
168 | success : function () {
169 | Opa5.assert.ok(true, "The line item list displays " + iNumberOfItems + " items");
170 | },
171 | errorMessage : "The line item list does not display " + iNumberOfItems + " items."
172 | });
173 | }
174 | });
175 | },
176 |
177 | iShouldSeeTheShareEmailButton : function () {
178 | return this.waitFor({
179 | id : "shareEmail",
180 | viewName : sViewName,
181 | success : function () {
182 | Opa5.assert.ok(true, "The E-Mail button is visible");
183 | },
184 | errorMessage : "The E-Mail button was not found"
185 | });
186 | }
187 | }
188 |
189 | }
190 |
191 | });
192 |
193 | });
--------------------------------------------------------------------------------
/src/localService/mockdata/LineItems.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "LineItemID" : "LineItemID_1",
4 | "ObjectID": "ObjectID_1",
5 | "Name" : "Line Item 1",
6 | "Attribute" : "Attribute A",
7 | "UnitOfMeasure" : "UoM",
8 | "UnitNumber" : 23
9 | }, {
10 | "LineItemID" : "LineItemID_2",
11 | "ObjectID": "ObjectID_1",
12 | "Name" : "Line Item 2",
13 | "Attribute" : "Attribute B",
14 | "UnitOfMeasure" : "UoM",
15 | "UnitNumber" : 42
16 | }, {
17 | "LineItemID" : "LineItemID_3",
18 | "ObjectID": "ObjectID_1",
19 | "Name" : "Line Item 3",
20 | "Attribute" : "Attribute C",
21 | "UnitOfMeasure" : "UoM",
22 | "UnitNumber" : 27
23 | }, {
24 | "LineItemID" : "LineItemID_4",
25 | "ObjectID": "ObjectID_1",
26 | "Name" : "Line Item 4",
27 | "Attribute" : "Attribute D",
28 | "UnitOfMeasure" : "UoM",
29 | "UnitNumber" : 76
30 | }, {
31 | "LineItemID" : "LineItemID_5",
32 | "ObjectID": "ObjectID_2",
33 | "Name" : "Line Item 5",
34 | "Attribute" : "Attribute E",
35 | "UnitOfMeasure" : "UoM",
36 | "UnitNumber" : 568
37 | }, {
38 | "LineItemID" : "LineItemID_6",
39 | "ObjectID": "ObjectID_3",
40 | "Name" : "Line Item 6",
41 | "Attribute" : "Attribute F",
42 | "UnitOfMeasure" : "UoM",
43 | "UnitNumber" : 453
44 | }, {
45 | "LineItemID" : "LineItemID_7",
46 | "ObjectID": "ObjectID_3",
47 | "Name" : "Line Item 7",
48 | "Attribute" : "Attribute G",
49 | "UnitOfMeasure" : "UoM",
50 | "UnitNumber" : 736
51 | }, {
52 | "LineItemID" : "LineItemID_8",
53 | "ObjectID": "ObjectID_3",
54 | "Name" : "Line Item 8",
55 | "Attribute" : "Attribute H",
56 | "UnitOfMeasure" : "UoM",
57 | "UnitNumber" : 123
58 | }, {
59 | "LineItemID" : "LineItemID_9",
60 | "ObjectID": "ObjectID_4",
61 | "Name" : "Line Item 9",
62 | "Attribute" : "Attribute I",
63 | "UnitOfMeasure" : "UoM",
64 | "UnitNumber" : 13
65 | }, {
66 | "LineItemID" : "LineItemID_10",
67 | "ObjectID": "ObjectID_4",
68 | "Name" : "Line Item 10",
69 | "Attribute" : "Attribute J",
70 | "UnitOfMeasure" : "UoM",
71 | "UnitNumber" : 23
72 | }, {
73 | "LineItemID" : "LineItemID_11",
74 | "ObjectID": "ObjectID_5",
75 | "Name" : "Line Item 11",
76 | "Attribute" : "Attribute K",
77 | "UnitOfMeasure" : "UoM",
78 | "UnitNumber" : 23
79 | }, {
80 | "LineItemID" : "LineItemID_12",
81 | "ObjectID": "ObjectID_5",
82 | "Name" : "Line Item 12",
83 | "Attribute" : "Attribute L",
84 | "UnitOfMeasure" : "UoM",
85 | "UnitNumber" : 123
86 | }, {
87 | "LineItemID" : "LineItemID_13",
88 | "ObjectID": "ObjectID_5",
89 | "Name" : "Line Item 13",
90 | "Attribute" : "Attribute M",
91 | "UnitOfMeasure" : "UoM",
92 | "UnitNumber" : 43
93 | }, {
94 | "LineItemID" : "LineItemID_14",
95 | "ObjectID": "ObjectID_6",
96 | "Name" : "Line Item 14",
97 | "Attribute" : "Attribute N",
98 | "UnitOfMeasure" : "UoM",
99 | "UnitNumber" : 37
100 | }, {
101 | "LineItemID" : "LineItemID_15",
102 | "ObjectID": "ObjectID_6",
103 | "Name" : "Line Item 15",
104 | "Attribute" : "Attribute O",
105 | "UnitOfMeasure" : "UoM",
106 | "UnitNumber" : 599
107 | }, {
108 | "LineItemID" : "LineItemID_16",
109 | "ObjectID": "ObjectID_6",
110 | "Name" : "Line Item 16",
111 | "Attribute" : "Attribute P",
112 | "UnitOfMeasure" : "UoM",
113 | "UnitNumber" : 7
114 | }, {
115 | "LineItemID" : "LineItemID_17",
116 | "ObjectID": "ObjectID_7",
117 | "Name" : "Line Item 17",
118 | "Attribute" : "Attribute Q",
119 | "UnitOfMeasure" : "UoM",
120 | "UnitNumber" : 31
121 | }, {
122 | "LineItemID" : "LineItemID_18",
123 | "ObjectID": "ObjectID_7",
124 | "Name" : "Line Item 18",
125 | "Attribute" : "Attribute R",
126 | "UnitOfMeasure" : "UoM",
127 | "UnitNumber" : 82
128 | }, {
129 | "LineItemID" : "LineItemID_19",
130 | "ObjectID": "ObjectID_7",
131 | "Name" : "Line Item 19",
132 | "Attribute" : "Attribute S",
133 | "UnitOfMeasure" : "UoM",
134 | "UnitNumber" : 19
135 | }, {
136 | "LineItemID" : "LineItemID_20",
137 | "ObjectID": "ObjectID_8",
138 | "Name" : "Line Item 20",
139 | "Attribute" : "Attribute T",
140 | "UnitOfMeasure" : "UoM",
141 | "UnitNumber" : 429
142 | }, {
143 | "LineItemID" : "LineItemID_21",
144 | "ObjectID": "ObjectID_8",
145 | "Name" : "Line Item 21",
146 | "Attribute" : "Attribute U",
147 | "UnitOfMeasure" : "UoM",
148 | "UnitNumber" : 43
149 | }, {
150 | "LineItemID" : "LineItemID_22",
151 | "ObjectID": "ObjectID_9",
152 | "Name" : "Line Item 22",
153 | "Attribute" : "Attribute V",
154 | "UnitOfMeasure" : "UoM",
155 | "UnitNumber" : 237
156 | }, {
157 | "LineItemID" : "LineItemID_23",
158 | "ObjectID": "ObjectID_9",
159 | "Name" : "Line Item 23",
160 | "Attribute" : "Attribute W",
161 | "UnitOfMeasure" : "UoM",
162 | "UnitNumber" : 723
163 | }, {
164 | "LineItemID" : "LineItemID_24",
165 | "ObjectID": "ObjectID_9",
166 | "Name" : "Line Item 24",
167 | "Attribute" : "Attribute X",
168 | "UnitOfMeasure" : "UoM",
169 | "UnitNumber" : 427
170 | }, {
171 | "LineItemID" : "LineItemID_25",
172 | "ObjectID": "ObjectID_9",
173 | "Name" : "Line Item 25",
174 | "Attribute" : "Attribute Y",
175 | "UnitOfMeasure" : "UoM",
176 | "UnitNumber" : 13
177 | }, {
178 | "LineItemID" : "LineItemID_26",
179 | "ObjectID": "ObjectID_10",
180 | "Name" : "Line Item 26",
181 | "Attribute" : "Attribute Z",
182 | "UnitOfMeasure" : "UoM",
183 | "UnitNumber" : 178
184 | }, {
185 | "LineItemID" : "LineItemID_27",
186 | "ObjectID": "ObjectID_11",
187 | "Name" : "Line Item 227",
188 | "Attribute" : "Attribute AA",
189 | "UnitOfMeasure" : "UoM",
190 | "UnitNumber" : 457
191 | }, {
192 | "LineItemID" : "LineItemID_28",
193 | "ObjectID": "ObjectID_11",
194 | "Name" : "Line Item 28",
195 | "Attribute" : "Attribute AB",
196 | "UnitOfMeasure" : "UoM",
197 | "UnitNumber" : 395
198 | }, {
199 | "LineItemID" : "LineItemID_29",
200 | "ObjectID": "ObjectID_12",
201 | "Name" : "Line Item 29",
202 | "Attribute" : "Attribute AC",
203 | "UnitOfMeasure" : "UoM",
204 | "UnitNumber" : 1379
205 | }, {
206 | "LineItemID" : "LineItemID_30",
207 | "ObjectID": "ObjectID_13",
208 | "Name" : "Line Item 30",
209 | "Attribute" : "Attribute AD",
210 | "UnitOfMeasure" : "UoM",
211 | "UnitNumber" : 406
212 | }, {
213 | "LineItemID" : "LineItemID_31",
214 | "ObjectID": "ObjectID_13",
215 | "Name" : "Line Item 31",
216 | "Attribute" : "Attribute AD",
217 | "UnitOfMeasure" : "UoM",
218 | "UnitNumber" : 504
219 | }, {
220 | "LineItemID" : "LineItemID_32",
221 | "ObjectID": "ObjectID_13",
222 | "Name" : "Line Item 32",
223 | "Attribute" : "Attribute AF",
224 | "UnitOfMeasure" : "UoM",
225 | "UnitNumber" : 483
226 | }, {
227 | "LineItemID" : "LineItemID_33",
228 | "ObjectID": "ObjectID_14",
229 | "Name" : "Line Item 33",
230 | "Attribute" : "Attribute AG",
231 | "UnitOfMeasure" : "UoM",
232 | "UnitNumber" : 2005
233 | }, {
234 | "LineItemID" : "LineItemID_34",
235 | "ObjectID": "ObjectID_15",
236 | "Name" : "Line Item 34",
237 | "Attribute" : "Attribute AH",
238 | "UnitOfMeasure" : "UoM",
239 | "UnitNumber" : 5
240 | }, {
241 | "LineItemID" : "LineItemID_35",
242 | "ObjectID": "ObjectID_16",
243 | "Name" : "Line Item 35",
244 | "Attribute" : "Attribute AI",
245 | "UnitOfMeasure" : "UoM",
246 | "UnitNumber" : 462
247 | }, {
248 | "LineItemID" : "LineItemID_36",
249 | "ObjectID": "ObjectID_16",
250 | "Name" : "Line Item 36",
251 | "Attribute" : "Attribute AJ",
252 | "UnitOfMeasure" : "UoM",
253 | "UnitNumber" : 237
254 | }, {
255 | "LineItemID" : "LineItemID_37",
256 | "ObjectID": "ObjectID_17",
257 | "Name" : "Line Item 37",
258 | "Attribute" : "Attribute AK",
259 | "UnitOfMeasure" : "UoM",
260 | "UnitNumber" : 4023
261 | }, {
262 | "LineItemID" : "LineItemID_38",
263 | "ObjectID": "ObjectID_17",
264 | "Name" : "Line Item 38",
265 | "Attribute" : "Attribute AL",
266 | "UnitOfMeasure" : "UoM",
267 | "UnitNumber" : 42
268 | }, {
269 | "LineItemID" : "LineItemID_39",
270 | "ObjectID": "ObjectID_18",
271 | "Name" : "Line Item 39",
272 | "Attribute" : "Attribute AM",
273 | "UnitOfMeasure" : "UoM",
274 | "UnitNumber" : 52
275 | }, {
276 | "LineItemID" : "LineItemID_40",
277 | "ObjectID": "ObjectID_20",
278 | "Name" : "Line Item 40",
279 | "Attribute" : "Attribute AN",
280 | "UnitOfMeasure" : "UoM",
281 | "UnitNumber" : 3
282 | }
283 | ]
--------------------------------------------------------------------------------
/src/controller/Master.controller.js:
--------------------------------------------------------------------------------
1 | /*global history */
2 | sap.ui.define([
3 | "typescript/example/ui5app/controller/BaseController",
4 | "sap/ui/model/json/JSONModel",
5 | "sap/ui/model/Filter",
6 | "sap/ui/model/FilterOperator",
7 | "sap/m/GroupHeaderListItem",
8 | "sap/ui/Device",
9 | "typescript/example/ui5app/model/formatter",
10 | "typescript/example/ui5app/model/grouper",
11 | "typescript/example/ui5app/model/GroupSortState"
12 | ], function (BaseController, JSONModel, Filter, FilterOperator, GroupHeaderListItem, Device, formatter, grouper, GroupSortState) {
13 | "use strict";
14 |
15 | return BaseController.extend("typescript.example.ui5app.controller.Master", {
16 |
17 | formatter: formatter,
18 |
19 | /* =========================================================== */
20 | /* lifecycle methods */
21 | /* =========================================================== */
22 |
23 | /**
24 | * Called when the master list controller is instantiated. It sets up the event handling for the master/detail communication and other lifecycle tasks.
25 | * @public
26 | */
27 | onInit : function () {
28 | // Control state model
29 | var oList = this.byId("list"),
30 | oViewModel = this._createViewModel(),
31 | // Put down master list's original value for busy indicator delay,
32 | // so it can be restored later on. Busy handling on the master list is
33 | // taken care of by the master list itself.
34 | iOriginalBusyDelay = oList.getBusyIndicatorDelay();
35 |
36 | this._oGroupSortState = new GroupSortState(oViewModel, grouper.groupUnitNumber(this.getResourceBundle()));
37 |
38 | this._oList = oList;
39 | // keeps the filter and search state
40 | this._oListFilterState = {
41 | aFilter : [],
42 | aSearch : []
43 | };
44 |
45 | this.setModel(oViewModel, "masterView");
46 | // Make sure, busy indication is showing immediately so there is no
47 | // break after the busy indication for loading the view's meta data is
48 | // ended (see promise 'oWhenMetadataIsLoaded' in AppController)
49 | oList.attachEventOnce("updateFinished", undefined, function(){
50 | // Restore original busy indicator delay for the list
51 | oViewModel.setProperty("/delay", iOriginalBusyDelay);
52 | });
53 |
54 | this.getView().addEventDelegate({
55 | onBeforeFirstShow: function () {
56 | this.getOwnerComponent().oListSelector.setBoundMasterList(oList);
57 | }.bind(this)
58 | });
59 |
60 | this.getRouter().getRoute("master").attachPatternMatched(this._onMasterMatched, this);
61 | this.getRouter().attachBypassed(this.onBypassed, this);
62 | },
63 |
64 | /* =========================================================== */
65 | /* event handlers */
66 | /* =========================================================== */
67 |
68 | /**
69 | * After list data is available, this handler method updates the
70 | * master list counter and hides the pull to refresh control, if
71 | * necessary.
72 | * @param {sap.ui.base.Event} oEvent the update finished event
73 | * @public
74 | */
75 | onUpdateFinished : function (oEvent) {
76 | // update the master list object counter after new data is loaded
77 | this._updateListItemCount(oEvent.getParameter("total"));
78 | // hide pull to refresh if necessary
79 | this.byId("pullToRefresh").hide();
80 | },
81 |
82 | /**
83 | * Event handler for the master search field. Applies current
84 | * filter value and triggers a new search. If the search field's
85 | * 'refresh' button has been pressed, no new search is triggered
86 | * and the list binding is refresh instead.
87 | * @param {sap.ui.base.Event} oEvent the search event
88 | * @public
89 | */
90 | onSearch : function (oEvent) {
91 | if (oEvent.getParameters().refreshButtonPressed) {
92 | // Search field's 'refresh' button has been pressed.
93 | // This is visible if you select any master list item.
94 | // In this case no new search is triggered, we only
95 | // refresh the list binding.
96 | this.onRefresh();
97 | return;
98 | }
99 |
100 | var sQuery = oEvent.getParameter("query");
101 |
102 | if (sQuery) {
103 | this._oListFilterState.aSearch = [new Filter("Name", FilterOperator.Contains, sQuery)];
104 | } else {
105 | this._oListFilterState.aSearch = [];
106 | }
107 | this._applyFilterSearch();
108 |
109 | },
110 |
111 | /**
112 | * Event handler for refresh event. Keeps filter, sort
113 | * and group settings and refreshes the list binding.
114 | * @public
115 | */
116 | onRefresh : function () {
117 | this._oList.getBinding("items").refresh();
118 | },
119 |
120 | /**
121 | * Event handler for the sorter selection.
122 | * @param {sap.ui.base.Event} oEvent the select event
123 | * @public
124 | */
125 | onSort : function (oEvent) {
126 | var sKey = oEvent.getSource().getSelectedItem().getKey(),
127 | aSorters = this._oGroupSortState.sort(sKey);
128 |
129 | this._applyGroupSort(aSorters);
130 | },
131 |
132 | /**
133 | * Event handler for the grouper selection.
134 | * @param {sap.ui.base.Event} oEvent the search field event
135 | * @public
136 | */
137 | onGroup : function (oEvent) {
138 | var sKey = oEvent.getSource().getSelectedItem().getKey(),
139 | aSorters = this._oGroupSortState.group(sKey);
140 |
141 | this._applyGroupSort(aSorters);
142 | },
143 |
144 | /**
145 | * Event handler for the filter button to open the ViewSettingsDialog.
146 | * which is used to add or remove filters to the master list. This
147 | * handler method is also called when the filter bar is pressed,
148 | * which is added to the beginning of the master list when a filter is applied.
149 | * @public
150 | */
151 | onOpenViewSettings : function () {
152 | if (!this._oViewSettingsDialog) {
153 | this._oViewSettingsDialog = sap.ui.xmlfragment("typescript.example.ui5app.view.ViewSettingsDialog", this);
154 | this.getView().addDependent(this._oViewSettingsDialog);
155 | // forward compact/cozy style into Dialog
156 | this._oViewSettingsDialog.addStyleClass(this.getOwnerComponent().getContentDensityClass());
157 | }
158 | this._oViewSettingsDialog.open();
159 | },
160 |
161 | /**
162 | * Event handler called when ViewSettingsDialog has been confirmed, i.e.
163 | * has been closed with 'OK'. In the case, the currently chosen filters
164 | * are applied to the master list, which can also mean that the currently
165 | * applied filters are removed from the master list, in case the filter
166 | * settings are removed in the ViewSettingsDialog.
167 | * @param {sap.ui.base.Event} oEvent the confirm event
168 | * @public
169 | */
170 | onConfirmViewSettingsDialog : function (oEvent) {
171 | var aFilterItems = oEvent.getParameters().filterItems,
172 | aFilters = [],
173 | aCaptions = [];
174 |
175 | // update filter state:
176 | // combine the filter array and the filter string
177 | aFilterItems.forEach(function (oItem) {
178 | switch (oItem.getKey()) {
179 | case "Filter1" :
180 | aFilters.push(new Filter("UnitNumber", FilterOperator.LE, 100));
181 | break;
182 | case "Filter2" :
183 | aFilters.push(new Filter("UnitNumber", FilterOperator.GT, 100));
184 | break;
185 | default :
186 | break;
187 | }
188 | aCaptions.push(oItem.getText());
189 | });
190 |
191 | this._oListFilterState.aFilter = aFilters;
192 | this._updateFilterBar(aCaptions.join(", "));
193 | this._applyFilterSearch();
194 | },
195 |
196 | /**
197 | * Event handler for the list selection event
198 | * @param {sap.ui.base.Event} oEvent the list selectionChange event
199 | * @public
200 | */
201 | onSelectionChange : function (oEvent) {
202 | // get the list item, either from the listItem parameter or from the event's source itself (will depend on the device-dependent mode).
203 | this._showDetail(oEvent.getParameter("listItem") || oEvent.getSource());
204 | },
205 |
206 | /**
207 | * Event handler for the bypassed event, which is fired when no routing pattern matched.
208 | * If there was an object selected in the master list, that selection is removed.
209 | * @public
210 | */
211 | onBypassed : function () {
212 | this._oList.removeSelections(true);
213 | },
214 |
215 | /**
216 | * Used to create GroupHeaders with non-capitalized caption.
217 | * These headers are inserted into the master list to
218 | * group the master list's items.
219 | * @param {Object} oGroup group whose text is to be displayed
220 | * @public
221 | * @returns {sap.m.GroupHeaderListItem} group header with non-capitalized caption.
222 | */
223 | createGroupHeader : function (oGroup) {
224 | return new GroupHeaderListItem({
225 | title : oGroup.text,
226 | upperCase : false
227 | });
228 | },
229 |
230 | /**
231 | * Event handler for navigating back.
232 | * We navigate back in the browser historz
233 | * @public
234 | */
235 | onNavBack : function() {
236 | history.go(-1);
237 | },
238 |
239 | /* =========================================================== */
240 | /* begin: internal methods */
241 | /* =========================================================== */
242 |
243 |
244 | _createViewModel : function() {
245 | return new JSONModel({
246 | isFilterBarVisible: false,
247 | filterBarLabel: "",
248 | delay: 0,
249 | title: this.getResourceBundle().getText("masterTitleCount", [0]),
250 | noDataText: this.getResourceBundle().getText("masterListNoDataText"),
251 | sortBy: "Name",
252 | groupBy: "None"
253 | });
254 | },
255 |
256 | /**
257 | * If the master route was hit (empty hash) we have to set
258 | * the hash to to the first item in the list as soon as the
259 | * listLoading is done and the first item in the list is known
260 | * @private
261 | */
262 | _onMasterMatched : function() {
263 | this.getOwnerComponent().oListSelector.oWhenListLoadingIsDone.then(
264 | function (mParams) {
265 | if (mParams.list.getMode() === "None") {
266 | return;
267 | }
268 | var sObjectId = mParams.firstListitem.getBindingContext().getProperty("ObjectID");
269 | this.getRouter().navTo("object", {objectId : sObjectId}, true);
270 | }.bind(this),
271 | function (mParams) {
272 | if (mParams.error) {
273 | return;
274 | }
275 | this.getRouter().getTargets().display("detailNoObjectsAvailable");
276 | }.bind(this)
277 | );
278 | },
279 |
280 | /**
281 | * Shows the selected item on the detail page
282 | * On phones a additional history entry is created
283 | * @param {sap.m.ObjectListItem} oItem selected Item
284 | * @private
285 | */
286 | _showDetail : function (oItem) {
287 | var bReplace = !Device.system.phone;
288 | this.getRouter().navTo("object", {
289 | objectId : oItem.getBindingContext().getProperty("ObjectID")
290 | }, bReplace);
291 | },
292 |
293 | /**
294 | * Sets the item count on the master list header
295 | * @param {integer} iTotalItems the total number of items in the list
296 | * @private
297 | */
298 | _updateListItemCount : function (iTotalItems) {
299 | var sTitle;
300 | // only update the counter if the length is final
301 | if (this._oList.getBinding("items").isLengthFinal()) {
302 | sTitle = this.getResourceBundle().getText("masterTitleCount", [iTotalItems]);
303 | this.getModel("masterView").setProperty("/title", sTitle);
304 | }
305 | },
306 |
307 | /**
308 | * Internal helper method to apply both filter and search state together on the list binding
309 | * @private
310 | */
311 | _applyFilterSearch : function () {
312 | var aFilters = this._oListFilterState.aSearch.concat(this._oListFilterState.aFilter),
313 | oViewModel = this.getModel("masterView");
314 | this._oList.getBinding("items").filter(aFilters, "Application");
315 | // changes the noDataText of the list in case there are no filter results
316 | if (aFilters.length !== 0) {
317 | oViewModel.setProperty("/noDataText", this.getResourceBundle().getText("masterListNoDataWithFilterOrSearchText"));
318 | } else if (this._oListFilterState.aSearch.length > 0) {
319 | // only reset the no data text to default when no new search was triggered
320 | oViewModel.setProperty("/noDataText", this.getResourceBundle().getText("masterListNoDataText"));
321 | }
322 | },
323 |
324 | /**
325 | * Internal helper method to apply both group and sort state together on the list binding
326 | * @param {sap.ui.model.Sorter[]} aSorters an array of sorters
327 | * @private
328 | */
329 | _applyGroupSort : function (aSorters) {
330 | this._oList.getBinding("items").sort(aSorters);
331 | },
332 |
333 | /**
334 | * Internal helper method that sets the filter bar visibility property and the label's caption to be shown
335 | * @param {string} sFilterBarText the selected filter value
336 | * @private
337 | */
338 | _updateFilterBar : function (sFilterBarText) {
339 | var oViewModel = this.getModel("masterView");
340 | oViewModel.setProperty("/isFilterBarVisible", (this._oListFilterState.aFilter.length > 0));
341 | oViewModel.setProperty("/filterBarLabel", this.getResourceBundle().getText("masterFilterBarText", [sFilterBarText]));
342 | }
343 |
344 | });
345 |
346 | }
347 | );
--------------------------------------------------------------------------------
/src/test/integration/pages/Master.js:
--------------------------------------------------------------------------------
1 | sap.ui.define([
2 | "sap/ui/test/Opa5",
3 | "sap/ui/test/actions/Press",
4 | "sap/ui/test/actions/EnterText",
5 | "typescript/example/ui5app/test/integration/pages/Common",
6 | "sap/ui/test/matchers/AggregationLengthEquals",
7 | "sap/ui/test/matchers/AggregationFilled",
8 | "sap/ui/test/matchers/PropertyStrictEquals"
9 | ], function(Opa5, Press, EnterText, Common, AggregationLengthEquals, AggregationFilled, PropertyStrictEquals) {
10 | "use strict";
11 |
12 | var sViewName = "Master",
13 | sSomethingThatCannotBeFound = "*#-Q@@||",
14 | iGroupingBoundary = 100;
15 |
16 | Opa5.createPageObjects({
17 | onTheMasterPage : {
18 | baseClass : Common,
19 |
20 | actions : {
21 |
22 | iWaitUntilTheListIsLoaded : function () {
23 | return this.waitFor({
24 | id : "list",
25 | viewName : sViewName,
26 | matchers : new AggregationFilled({name : "items"}),
27 | errorMessage : "The master list has not been loaded"
28 | });
29 | },
30 |
31 | iWaitUntilTheFirstItemIsSelected : function () {
32 | return this.waitFor({
33 | id : "list",
34 | viewName : sViewName,
35 | matchers : function(oList) {
36 | // wait until the list has a selected item
37 | var oSelectedItem = oList.getSelectedItem();
38 | return oSelectedItem && oList.getItems().indexOf(oSelectedItem) === 0;
39 | },
40 | errorMessage : "The first item of the master list is not selected"
41 | });
42 | },
43 |
44 | iSortTheListOnName : function () {
45 | return this.iPressItemInSelectInFooter("sort-select", "masterSort1");
46 | },
47 |
48 | iSortTheListOnUnitNumber : function () {
49 | return this.iPressItemInSelectInFooter("sort-select", "masterSort2");
50 | },
51 |
52 | iRemoveFilterFromTheList : function () {
53 | return this.iPressItemInSelectInFooter("filter-select", "masterFilterNone");
54 | },
55 |
56 | iFilterTheListLessThan100UoM : function () {
57 | return this.iPressItemInSelectInFooter("filter-select", "masterFilter1");
58 | },
59 |
60 | iFilterTheListMoreThan100UoM : function () {
61 | return this.iPressItemInSelectInFooter("filter-select", "masterFilter2");
62 | },
63 |
64 | iGroupTheList : function () {
65 | return this.iPressItemInSelectInFooter("group-select", "masterGroup1");
66 | },
67 |
68 | iRemoveListGrouping : function () {
69 | return this.iPressItemInSelectInFooter("group-select", "masterGroupNone");
70 | },
71 |
72 | iOpenViewSettingsDialog : function () {
73 | return this.waitFor({
74 | id : "filter-button",
75 | viewName : sViewName,
76 | check : function () {
77 | var oViewSettingsDialog = Opa5.getWindow().sap.ui.getCore().byId("viewSettingsDialog");
78 | // check if the dialog is still open - wait until it is closed
79 | // view settings dialog has no is open function and no open close events so checking the domref is the only option here
80 | // if there is no view settings dialog yet, there is no need to wait
81 | return !oViewSettingsDialog || oViewSettingsDialog.$().length === 0;
82 | },
83 | actions : new Press(),
84 | errorMessage : "Did not find the 'filter' button."
85 | });
86 | },
87 |
88 | iSelectListItemInViewSettingsDialog : function (sListItemTitle) {
89 | return this.waitFor({
90 | searchOpenDialogs : true,
91 | controlType : "sap.m.StandardListItem",
92 | matchers : new Opa5.matchers.PropertyStrictEquals({name : "title", value : sListItemTitle}),
93 | actions: new Press(),
94 | errorMessage : "Did not find list item with title " + sListItemTitle + " in View Settings Dialog."
95 | });
96 | },
97 |
98 | iPressOKInViewSelectionDialog : function () {
99 | return this.waitFor({
100 | searchOpenDialogs : true,
101 | controlType : "sap.m.Button",
102 | matchers : new Opa5.matchers.PropertyStrictEquals({name : "text", value : "OK"}),
103 | actions : new Press(),
104 | errorMessage : "Did not find the ViewSettingDialog's 'OK' button."
105 | });
106 | },
107 |
108 | iPressResetInViewSelectionDialog : function () {
109 | return this.waitFor({
110 | searchOpenDialogs : true,
111 | controlType : "sap.m.Button",
112 | matchers : new Opa5.matchers.PropertyStrictEquals({name : "icon", value : "sap-icon://refresh"}),
113 | actions : new Press(),
114 | errorMessage : "Did not find the ViewSettingDialog's 'Reset' button."
115 | });
116 | },
117 |
118 | iPressItemInSelectInFooter : function (sSelect, sItem) {
119 | return this.waitFor({
120 | id : sSelect,
121 | viewName : sViewName,
122 | success : function (oSelect) {
123 | oSelect.open();
124 | this.waitFor({
125 | id : sItem,
126 | viewName : sViewName,
127 | success : function(oElem){
128 | oElem.$().trigger("tap");
129 | },
130 | errorMessage : "Did not find the " + sItem + " element in select"
131 | });
132 | }.bind(this),
133 | errorMessage : "Did not find the " + sSelect + " select"
134 | });
135 | },
136 |
137 | iRememberTheSelectedItem : function () {
138 | return this.waitFor({
139 | id : "list",
140 | viewName : sViewName,
141 | matchers : function (oList) {
142 | return oList.getSelectedItem();
143 | },
144 | success : function (oListItem) {
145 | this.iRememberTheListItem(oListItem);
146 | },
147 | errorMessage : "The list does not have a selected item so nothing can be remembered"
148 | });
149 | },
150 |
151 | iRememberTheIdOfListItemAtPosition : function (iPosition) {
152 | return this.waitFor({
153 | id : "list",
154 | viewName : sViewName,
155 | matchers : function (oList) {
156 | return oList.getItems()[iPosition];
157 | },
158 | success : function (oListItem) {
159 | this.iRememberTheListItem(oListItem);
160 | },
161 | errorMessage : "The list does not have an item at the index " + iPosition
162 | });
163 | },
164 |
165 | iRememberAnIdOfAnObjectThatsNotInTheList : function () {
166 | return this.waitFor(this.createAWaitForAnEntitySet({
167 | entitySet : "Objects",
168 | success : function (aEntityData) {
169 | this.waitFor({
170 | id : "list",
171 | viewName : sViewName,
172 | matchers : new AggregationFilled({name: "items"}),
173 | success : function (oList) {
174 | var sCurrentId,
175 | aItemsNotInTheList = aEntityData.filter(function (oObject) {
176 | return !oList.getItems().some(function (oListItem) {
177 | return oListItem.getBindingContext().getProperty("ObjectID") === oObject.ObjectID;
178 | });
179 | });
180 |
181 | if (!aItemsNotInTheList.length) {
182 | // Not enough items all of them are displayed so we take the last one
183 | sCurrentId = aEntityData[aEntityData.length - 1].ObjectID;
184 | } else {
185 | sCurrentId = aItemsNotInTheList[0].ObjectID;
186 | }
187 |
188 | var oCurrentItem = this.getContext().currentItem;
189 | // Construct a binding path since the list item is not created yet and we only have the id.
190 | oCurrentItem.bindingPath = "/" + oList.getModel().createKey("Objects", {
191 | ObjectID : sCurrentId
192 | });
193 | oCurrentItem.id = sCurrentId;
194 | },
195 | errorMessage : "the model does not have a item that is not in the list"
196 | });
197 | }
198 | }));
199 | },
200 |
201 | iPressOnTheObjectAtPosition : function (iPositon) {
202 | return this.waitFor({
203 | id : "list",
204 | viewName : sViewName,
205 | matchers : function (oList) {
206 | return oList.getItems()[iPositon];
207 | },
208 | actions : new Press(),
209 | errorMessage : "List 'list' in view '" + sViewName + "' does not contain an ObjectListItem at position '" + iPositon + "'"
210 | });
211 | },
212 |
213 | iSearchForTheFirstObject : function (){
214 | var sFirstObjectTitle;
215 |
216 | return this.waitFor({
217 | id : "list",
218 | viewName : sViewName,
219 | matchers: new AggregationFilled({name : "items"}),
220 | success : function (oList) {
221 | sFirstObjectTitle = oList.getItems()[0].getTitle();
222 | return this.iSearchForValue(new EnterText({text: sFirstObjectTitle}), new Press());
223 | },
224 | errorMessage : "Did not find list items while trying to search for the first item."
225 | });
226 | },
227 |
228 | iTypeSomethingInTheSearchThatCannotBeFoundAndTriggerRefresh : function () {
229 | var fireRefreshButtonPressedOnSearchField = function (oSearchField) {
230 |
231 | /*eslint-disable new-cap */
232 | var oEvent = jQuery.Event("touchend");
233 | /*eslint-enable new-cap */
234 | oEvent.originalEvent = {refreshButtonPressed: true, id: oSearchField.getId()};
235 | oEvent.target = oSearchField;
236 | oEvent.srcElement = oSearchField;
237 | jQuery.extend(oEvent, oEvent.originalEvent);
238 |
239 | oSearchField.fireSearch(oEvent);
240 | };
241 | return this.iSearchForValue([new EnterText({text: sSomethingThatCannotBeFound}), fireRefreshButtonPressedOnSearchField]);
242 | },
243 |
244 | iSearchForValue : function (aActions) {
245 | return this.waitFor({
246 | id : "searchField",
247 | viewName : sViewName,
248 | actions: aActions,
249 | errorMessage : "Failed to find search field in Master view.'"
250 | });
251 | },
252 |
253 | iClearTheSearch : function () {
254 | //can not use 'EnterText' action to enter empty strings (yet)
255 | var fnClearSearchField = function(oSearchField) {
256 | oSearchField.clear();
257 | };
258 | return this.iSearchForValue([fnClearSearchField]);
259 | },
260 |
261 | iSearchForSomethingWithNoResults : function () {
262 | return this.iSearchForValue([new EnterText({text: sSomethingThatCannotBeFound}), new Press()]);
263 | },
264 |
265 | iRememberTheListItem : function (oListItem) {
266 | var oBindingContext = oListItem.getBindingContext();
267 | this.getContext().currentItem = {
268 | bindingPath: oBindingContext.getPath(),
269 | id: oBindingContext.getProperty("ObjectID"),
270 | title: oBindingContext.getProperty("Name")
271 | };
272 | }
273 | },
274 |
275 | assertions : {
276 |
277 | iShouldSeeTheBusyIndicator : function () {
278 | return this.waitFor({
279 | id : "list",
280 | viewName : sViewName,
281 | success : function (oList) {
282 | // we set the list busy, so we need to query the parent of the app
283 | Opa5.assert.ok(oList.getBusy(), "The master list is busy");
284 | },
285 | errorMessage : "The master list is not busy."
286 | });
287 | },
288 |
289 | theListGroupShouldBeFilteredOnUnitNumberValue20OrLess : function () {
290 | return this.theListShouldBeFilteredOnUnitNumberValue(20, false, {iLow : 1, iHigh : 2});
291 | },
292 |
293 | theListShouldContainAGroupHeader : function () {
294 | return this.waitFor({
295 | controlType : "sap.m.GroupHeaderListItem",
296 | viewName : sViewName,
297 | success : function () {
298 | Opa5.assert.ok(true, "Master list is grouped");
299 | },
300 | errorMessage : "Master list is not grouped"
301 | });
302 | },
303 |
304 | theListShouldContainOnlyFormattedUnitNumbers : function () {
305 | return this.theUnitNumbersShouldHaveTwoDecimals("sap.m.ObjectListItem",
306 | sViewName,
307 | "Numbers in ObjectListItems numbers are properly formatted",
308 | "List has no entries which can be checked for their formatting");
309 | },
310 |
311 | theListHeaderDisplaysZeroHits : function () {
312 | return this.waitFor({
313 | viewName : sViewName,
314 | id : "page",
315 | matchers : new PropertyStrictEquals({name : "title", value : " (0)"}),
316 | success : function () {
317 | Opa5.assert.ok(true, "The list header displays zero hits");
318 | },
319 | errorMessage : "The list header still has items"
320 | });
321 | },
322 |
323 | theListHasEntries : function () {
324 | return this.waitFor({
325 | viewName : sViewName,
326 | id : "list",
327 | matchers : new AggregationFilled({
328 | name : "items"
329 | }),
330 | success : function () {
331 | Opa5.assert.ok(true, "The list has items");
332 | },
333 | errorMessage : "The list had no items"
334 | });
335 | },
336 |
337 | theListShouldNotContainGroupHeaders : function () {
338 | function fnIsGroupHeader (oElement) {
339 | return oElement.getMetadata().getName() === "sap.m.GroupHeaderListItem";
340 | }
341 |
342 | return this.waitFor({
343 | viewName : sViewName,
344 | id : "list",
345 | matchers : function (oList) {
346 | return !oList.getItems().some(fnIsGroupHeader);
347 | },
348 | success : function() {
349 | Opa5.assert.ok(true, "Master list does not contain a group header although grouping has been removed.");
350 | },
351 | errorMessage : "Master list still contains a group header although grouping has been removed."
352 | });
353 | },
354 |
355 | theListShouldBeSortedAscendingOnUnitNumber : function () {
356 | return this.theListShouldBeSortedAscendingOnField("UnitNumber");
357 | },
358 |
359 | theListShouldBeSortedAscendingOnName : function () {
360 | return this.theListShouldBeSortedAscendingOnField("Name");
361 | },
362 |
363 | theListShouldBeSortedAscendingOnField : function (sField) {
364 | function fnCheckSort (oList){
365 | var oLastValue = null,
366 | fnIsOrdered = function (oElement) {
367 | if (!oElement.getBindingContext()) {
368 | return false;
369 | }
370 |
371 | var oCurrentValue = oElement.getBindingContext().getProperty(sField);
372 |
373 | if (oCurrentValue === undefined) {
374 | return false;
375 | }
376 |
377 | if (!oLastValue || oCurrentValue >= oLastValue){
378 | oLastValue = oCurrentValue;
379 | } else {
380 | return false;
381 | }
382 | return true;
383 | };
384 |
385 | return oList.getItems().every(fnIsOrdered);
386 | }
387 |
388 | return this.waitFor({
389 | viewName : sViewName,
390 | id : "list",
391 | matchers : fnCheckSort,
392 | success : function() {
393 | Opa5.assert.ok(true, "Master list has been sorted correctly for field '" + sField + "'.");
394 | },
395 | errorMessage : "Master list has not been sorted correctly for field '" + sField + "'."
396 | });
397 | },
398 |
399 | theListShouldBeFilteredOnUnitNumberValue : function(iThreshhold, bGreaterThan, oRange) {
400 |
401 | function fnCheckFilter (oList){
402 | var fnIsGreaterThanMaxValue = function (oElement) {
403 | if (bGreaterThan) {
404 | return oElement.getBindingContext().getProperty("UnitNumber") < iThreshhold;
405 | }
406 | return oElement.getBindingContext().getProperty("UnitNumber") > iThreshhold;
407 | };
408 | var aItems = oList.getItems();
409 | if (oRange) {
410 | aItems = aItems.slice(oRange.iLow, oRange.iHigh);
411 | }
412 |
413 | return !aItems.some(fnIsGreaterThanMaxValue);
414 | }
415 |
416 | return this.waitFor({
417 | id : "list",
418 | viewName : sViewName,
419 | matchers : fnCheckFilter,
420 | success : function(){
421 | Opa5.assert.ok(true, "Master list has been filtered correctly with filter value '" + iThreshhold + "'.");
422 | },
423 | errorMessage : "Master list has not been filtered correctly with filter value '" + iThreshhold + "'."
424 | });
425 | },
426 |
427 | theMasterListShouldBeFilteredOnUnitNumberValueMoreThanTheGroupBoundary : function(){
428 | return this.theListShouldBeFilteredOnUnitNumberValue(iGroupingBoundary, true);
429 | },
430 |
431 | theMasterListShouldBeFilteredOnUnitNumberValueLessThanTheGroupBoundary : function(){
432 | return this.theListShouldBeFilteredOnUnitNumberValue(iGroupingBoundary);
433 | },
434 |
435 | iShouldSeeTheList : function () {
436 | return this.waitFor({
437 | id : "list",
438 | viewName : sViewName,
439 | success : function (oList) {
440 | Opa5.assert.ok(oList, "Found the object List");
441 | },
442 | errorMessage : "Can't see the master list."
443 | });
444 | },
445 |
446 | theListShowsOnlyObjectsWithTheSearchStringInTheirTitle : function () {
447 | this.waitFor({
448 | id : "list",
449 | viewName : sViewName,
450 | matchers : new AggregationFilled({name : "items"}),
451 | check : function(oList) {
452 | var sTitle = oList.getItems()[0].getTitle(),
453 | bEveryItemContainsTheTitle = oList.getItems().every(function (oItem) {
454 | return oItem.getTitle().indexOf(sTitle) !== -1;
455 | });
456 | return bEveryItemContainsTheTitle;
457 | },
458 | success : function (oList) {
459 | Opa5.assert.ok(true, "Every item did contain the title");
460 | },
461 | errorMessage : "The list did not have items"
462 | });
463 | },
464 |
465 | theListShouldHaveNEntries : function (iObjIndex) {
466 | return this.waitFor({
467 | id : "list",
468 | viewName : sViewName,
469 | matchers : [ new AggregationLengthEquals({name : "items", length : iObjIndex}) ],
470 | success : function (oList) {
471 | Opa5.assert.strictEqual(oList.getItems().length, iObjIndex, "The list has x items");
472 | },
473 | errorMessage : "List does not have " + iObjIndex + " entries."
474 | });
475 | },
476 |
477 | theListShouldHaveAllEntries : function () {
478 | var aAllEntities,
479 | iExpectedNumberOfItems;
480 | // retrieve all Objects to be able to check for the total amount
481 | this.waitFor(this.createAWaitForAnEntitySet({
482 | entitySet : "Objects",
483 | success : function (aEntityData) {
484 | aAllEntities = aEntityData;
485 | }
486 | }));
487 |
488 | return this.waitFor({
489 | id : "list",
490 | viewName : sViewName,
491 | matchers : function (oList) {
492 | // If there are less items in the list than the growingThreshold, only check for this number.
493 | iExpectedNumberOfItems = Math.min(oList.getGrowingThreshold(), aAllEntities.length);
494 | return new AggregationLengthEquals({name : "items", length : iExpectedNumberOfItems}).isMatching(oList);
495 | },
496 | success : function (oList) {
497 | Opa5.assert.strictEqual(oList.getItems().length, iExpectedNumberOfItems, "The growing list displays all items");
498 | },
499 | errorMessage : "List does not display all entries."
500 | });
501 | },
502 |
503 | iShouldSeeTheNoDataTextForNoSearchResults : function () {
504 | return this.waitFor({
505 | id : "list",
506 | viewName : sViewName,
507 | success : function (oList) {
508 | Opa5.assert.strictEqual(oList.getNoDataText(), oList.getModel("i18n").getProperty("masterListNoDataWithFilterOrSearchText"), "the list should show the no data text for search and filter");
509 | },
510 | errorMessage : "list does not show the no data text for search and filter"
511 | });
512 | },
513 |
514 | theHeaderShouldDisplayAllEntries : function () {
515 | return this.waitFor({
516 | id : "list",
517 | viewName : sViewName,
518 | success : function (oList) {
519 | var iExpectedLength = oList.getBinding("items").getLength();
520 | this.waitFor({
521 | id : "page",
522 | viewName : sViewName,
523 | matchers : new PropertyStrictEquals({name : "title", value : " (" + iExpectedLength + ")"}),
524 | success : function () {
525 | Opa5.assert.ok(true, "The master page header displays " + iExpectedLength + " items");
526 | },
527 | errorMessage : "The master page header does not display " + iExpectedLength + " items."
528 | });
529 | },
530 | errorMessage : "Header does not display the number of items in the list"
531 | });
532 | },
533 |
534 | theFirstItemShouldBeSelected : function () {
535 | return this.waitFor({
536 | id : "list",
537 | viewName : sViewName,
538 | matchers : new AggregationFilled({name : "items"}),
539 | success : function (oList) {
540 | Opa5.assert.strictEqual(oList.getItems()[0], oList.getSelectedItem(), "The first object is selected");
541 | },
542 | errorMessage : "The first object is not selected."
543 | });
544 | },
545 |
546 | theListShouldHaveNoSelection : function () {
547 | return this.waitFor({
548 | id : "list",
549 | viewName : sViewName,
550 | matchers : function(oList) {
551 | return !oList.getSelectedItem();
552 | },
553 | success : function (oList) {
554 | Opa5.assert.strictEqual(oList.getSelectedItems().length, 0, "The list selection is removed");
555 | },
556 | errorMessage : "List selection was not removed"
557 | });
558 | },
559 |
560 | theRememberedListItemShouldBeSelected : function () {
561 | this.waitFor({
562 | id : "list",
563 | viewName : sViewName,
564 | matchers : function (oList) {
565 | return oList.getSelectedItem();
566 | },
567 | success : function (oSelectedItem) {
568 | Opa5.assert.strictEqual(oSelectedItem.getTitle(), this.getContext().currentItem.title, "The list selection is incorrect");
569 | },
570 | errorMessage : "The list has no selection"
571 | });
572 | }
573 |
574 | }
575 |
576 | }
577 |
578 | });
579 |
580 | });
--------------------------------------------------------------------------------