From bd4e2fc76cf736e3b40fa821953a60ef2e71fb32 Mon Sep 17 00:00:00 2001 From: Thorsten Hochreuter Date: Fri, 5 Oct 2018 11:20:23 +0200 Subject: [PATCH] [FEATURE] Adds public API for loadData promise - Adds an additional Promise to support checking for the data loading state. Currently, if the JSONModel was created from the manifest, applications cannot check if the data was loaded yet during the controller init. Attaching to the requestCompleted events etc. might be too late, depending on the timing of the data requests. - Also adds a Promise as the return value of the loadData() function. Change-Id: I99d1079b907bc156efaa833c7d20b1e04e0451b2 Fixes: https://github.com/SAP/openui5/issues/2213 Fixes: https://github.com/SAP/openui5/issues/2282 BCP: 1880574213 --- .../src/sap/ui/model/json/JSONModel.js | 37 +++- .../sap/ui/core/qunit/json/JSONModel.qunit.js | 200 +++++++++++++++++- .../qunit/json/data/JSONModelFakeService.js | 9 +- 3 files changed, 238 insertions(+), 8 deletions(-) diff --git a/src/sap.ui.core/src/sap/ui/model/json/JSONModel.js b/src/sap.ui.core/src/sap/ui/model/json/JSONModel.js index 0b6984f275f4..d9eaf3e347ef 100644 --- a/src/sap.ui.core/src/sap/ui/model/json/JSONModel.js +++ b/src/sap.ui.core/src/sap/ui/model/json/JSONModel.js @@ -192,6 +192,7 @@ sap.ui.define([ * @param {boolean} [bCache=true] Disables caching if set to false. Default is true. * @param {object} [mHeaders] An object of additional header key/value pairs to send along with the request * + * @return {Promise|undefined} in case bAsync is set to true a Promise is returned; this promise resolves/rejects based on the request status * @public */ JSONModel.prototype.loadData = function(sURL, oParameters, bAsync, sType, bMerge, bCache, mHeaders){ @@ -233,6 +234,10 @@ sap.ui.define([ this.fireRequestCompleted({url : sURL, type : sType, async : bAsync, headers: mHeaders, info : "cache=" + bCache + ";bMerge=" + bMerge, infoObject: {cache : bCache, merge : bMerge}, success: false, errorobject: oError}); this.fireRequestFailed(oError); + + if (bAsync) { + return Promise.reject(oError); + } }.bind(this); var _loadData = function(fnSuccess, fnError) { @@ -257,17 +262,39 @@ sap.ui.define([ _loadData(resolve, fnReject); }); - this.pSequentialImportCompleted = this.pSequentialImportCompleted.then(function() { - //must always resolve - return pImportCompleted.then(fnSuccess, fnError).catch(function(oError) { - Log.error("Loading of data failed: " + oError.stack); - }); + // chain the existing loadData calls, so the import is done sequentially + var pReturn = this.pSequentialImportCompleted.then(function() { + return pImportCompleted.then(fnSuccess, fnError); }); + + // attach exception/rejection handler, so the internal import promise always resolves + this.pSequentialImportCompleted = pReturn.catch(function(oError) { + Log.error("Loading of data failed: " + oError.stack); + }); + + // return chained loadData promise (sequential imports) + // but without a catch handler, so the application can also is notified about request failures + return pReturn; } else { _loadData(fnSuccess, fnError); } }; + /** + * Returns a Promise of the current data-loading state. + * Every currently running {@link sap.ui.model.json.JSONModel#loadData} call is respected by the returned Promise. + * This also includes a potential loadData call from the JSONModel's constructor in case a URL was given. + * The data-loaded Promise will resolve once all running requests have finished. + * Only request, which have been queued up to the point of calling + * this function will be respected by the returned Promise. + * + * @return {Promise} a Promise, which resolves if all pending data-loading requests have finished + * @public + */ + JSONModel.prototype.dataLoaded = function() { + return this.pSequentialImportCompleted; + }; + /** * @see sap.ui.model.Model.prototype.bindProperty * diff --git a/src/sap.ui.core/test/sap/ui/core/qunit/json/JSONModel.qunit.js b/src/sap.ui.core/test/sap/ui/core/qunit/json/JSONModel.qunit.js index 4cf52df01d26..5f58398a0ae4 100644 --- a/src/sap.ui.core/test/sap/ui/core/qunit/json/JSONModel.qunit.js +++ b/src/sap.ui.core/test/sap/ui/core/qunit/json/JSONModel.qunit.js @@ -471,7 +471,19 @@ sap.ui.define([ }); }); - QUnit.test("test JSONModel loadData: multiple requests - merge",function(assert){ + QUnit.test("test JSONModel loadData: dataLoaded() [async, Promise(chained)]",function(assert){ + var done = assert.async(); + var testModel = new JSONModel(); + testModel.loadData("test-resources/sap/ui/core/qunit/json/data/testdata.json"); + testModel.dataLoaded().then(function() { + assert.equal(testModel.getProperty("/foo"), "The quick brown fox jumps over the lazy dog."); + assert.equal(testModel.getProperty("/bar"), "ABCDEFG"); + assert.equal(testModel.getProperty("/baz")[1], 97); + done(); // resume normal testing + }); + }); + + QUnit.test("test JSONModel loadData [async, event]: multiple requests - merge",function(assert){ var done = assert.async(); var testModel = new JSONModel(); var loadCount = 0; @@ -494,7 +506,75 @@ sap.ui.define([ }); }); - QUnit.test("test JSONModel loadData: multiple requests",function(assert){ + QUnit.test("test JSONModel loadData [async, Promise(chained)]: multiple requests - merge",function(assert){ + var done = assert.async(); + var testModel = new JSONModel(); + testModel.loadData("test-resources/sap/ui/core/qunit/json/data/testdata.json"); + testModel.loadData("test-resources/sap/ui/core/qunit/json/data/testdata2.json",null,true,null,true); + // once the Promise resolves, everything is already merged + testModel.dataLoaded().then(function() { + assert.equal(testModel.getProperty("/foo"), "The quick brown fox jumps over the lazy dog."); + assert.equal(testModel.getProperty("/bar"), "ABCDEFGHIJ"); + assert.equal(testModel.getProperty("/baz")[1], 97); + assert.equal(testModel.getProperty("/merged"), true); + done(); + }); + }); + + QUnit.test("test JSONModel loadData [async, event & Promise]: multiple requests - merge",function(assert){ + var done = assert.async(); + var testModel = new JSONModel(); + var loadCount = 0; + testModel.loadData("test-resources/sap/ui/core/qunit/json/data/testdata.json"); + testModel.loadData("test-resources/sap/ui/core/qunit/json/data/testdata2.json",null,true,null,true); + // one event handler call for each loadData call + testModel.attachRequestCompleted(function() { + loadCount++; + if (loadCount == 1) { + assert.equal(testModel.getProperty("/foo"), "The quick brown fox jumps over the lazy dog."); + assert.equal(testModel.getProperty("/bar"), "ABCDEFG"); + assert.equal(testModel.getProperty("/baz")[1], 97); + } else { + assert.equal(testModel.getProperty("/foo"), "The quick brown fox jumps over the lazy dog."); + assert.equal(testModel.getProperty("/bar"), "ABCDEFGHIJ"); + assert.equal(testModel.getProperty("/baz")[1], 97); + assert.equal(testModel.getProperty("/merged"), true); + } + }); + // Only one promise for ALL loadData calls; + // resolve: everything is already merged, the intermediate states are no seen anymore + testModel.dataLoaded().then(function() { + assert.equal(testModel.getProperty("/foo"), "The quick brown fox jumps over the lazy dog."); + assert.equal(testModel.getProperty("/bar"), "ABCDEFGHIJ"); + assert.equal(testModel.getProperty("/baz")[1], 97); + assert.equal(testModel.getProperty("/merged"), true); + done(); + }); + }); + + QUnit.test("test JSONModel loadData [async, Promise(single)]: multiple requests - merge",function(assert){ + var done = assert.async(); + var testModel = new JSONModel(); + + var pLoad1 = testModel.loadData("test-resources/sap/ui/core/qunit/json/data/testdata.json").then(function() { + assert.equal(testModel.getProperty("/foo"), "The quick brown fox jumps over the lazy dog."); + assert.equal(testModel.getProperty("/bar"), "ABCDEFG"); + assert.equal(testModel.getProperty("/baz")[1], 97); + }); + + var pLoad2 = testModel.loadData("test-resources/sap/ui/core/qunit/json/data/testdata2.json",null,true,null,true).then(function() { + assert.equal(testModel.getProperty("/foo"), "The quick brown fox jumps over the lazy dog."); + assert.equal(testModel.getProperty("/bar"), "ABCDEFGHIJ"); + assert.equal(testModel.getProperty("/baz")[1], 97); + assert.equal(testModel.getProperty("/merged"), true); + }); + + Promise.all([pLoad1, pLoad2]).then(function() { + done(); + }); + }); + + QUnit.test("test JSONModel loadData [async, event]: multiple requests - no merge",function(assert){ var done = assert.async(); var testModel = new JSONModel(); var loadCount = 0; @@ -517,6 +597,122 @@ sap.ui.define([ }); }); + QUnit.test("test JSONModel loadData [async, Promise]: multiple requests - no merge",function(assert){ + var done = assert.async(); + var testModel = new JSONModel(); + testModel.loadData("test-resources/sap/ui/core/qunit/json/data/testdata.json"); + testModel.loadData("test-resources/sap/ui/core/qunit/json/data/testdata2.json"); + testModel.dataLoaded().then(function() { + assert.ok(!testModel.getProperty("/foo"), "deleted as no merge"); + assert.equal(testModel.getProperty("/bar"), "ABCDEFGHIJ"); + assert.ok(!testModel.getProperty("/baz"), "deleted as no merge"); + assert.equal(testModel.getProperty("/merged"), true); + done(); + }); + }); + + QUnit.test("test JSONModel loadData [async, event & Promise]: multiple requests - no merge",function(assert){ + var done = assert.async(); + var testModel = new JSONModel(); + var loadCount = 0; + testModel.loadData("test-resources/sap/ui/core/qunit/json/data/testdata.json"); + testModel.loadData("test-resources/sap/ui/core/qunit/json/data/testdata2.json"); + + // one event handler call for each loadData call + testModel.attachRequestCompleted(function() { + loadCount++; + if (loadCount == 1) { + assert.equal(testModel.getProperty("/foo"), "The quick brown fox jumps over the lazy dog."); + assert.equal(testModel.getProperty("/bar"), "ABCDEFG"); + assert.equal(testModel.getProperty("/baz")[1], 97); + } else { + assert.ok(!testModel.getProperty("/foo"), "deleted as no merge"); + assert.equal(testModel.getProperty("/bar"), "ABCDEFGHIJ"); + assert.ok(!testModel.getProperty("/baz"), "deleted as no merge"); + assert.equal(testModel.getProperty("/merged"), true); + } + }); + + // Only one promise for ALL loadData calls; + // resolve: everything is already merged, the intermediate states are no seen anymore + testModel.dataLoaded().then(function() { + assert.ok(!testModel.getProperty("/foo"), "deleted as no merge"); + assert.equal(testModel.getProperty("/bar"), "ABCDEFGHIJ"); + assert.ok(!testModel.getProperty("/baz"), "deleted as no merge"); + assert.equal(testModel.getProperty("/merged"), true); + done(); + }); + }); + + QUnit.test("test JSONModel loadData [async, Promise(single)]: multiple requests - no merge",function(assert){ + var done = assert.async(); + var testModel = new JSONModel(); + + var p1 = testModel.loadData("test-resources/sap/ui/core/qunit/json/data/testdata.json").then(function() { + assert.equal(testModel.getProperty("/foo"), "The quick brown fox jumps over the lazy dog."); + assert.equal(testModel.getProperty("/bar"), "ABCDEFG"); + assert.equal(testModel.getProperty("/baz")[1], 97); + }); + + var p2 = testModel.loadData("test-resources/sap/ui/core/qunit/json/data/testdata2.json").then(function() { + assert.ok(!testModel.getProperty("/foo"), "deleted as no merge"); + assert.equal(testModel.getProperty("/bar"), "ABCDEFGHIJ"); + assert.ok(!testModel.getProperty("/baz"), "deleted as no merge"); + assert.equal(testModel.getProperty("/merged"), true); + }); + + // Only one promise for ALL loadData calls; + // resolve: everything is already merged, the intermediate states are no seen anymore + Promise.all([p1, p2]).then(function() { + assert.ok(!testModel.getProperty("/foo"), "deleted as no merge"); + assert.equal(testModel.getProperty("/bar"), "ABCDEFGHIJ"); + assert.ok(!testModel.getProperty("/baz"), "deleted as no merge"); + assert.equal(testModel.getProperty("/merged"), true); + done(); + }); + }); + + QUnit.test("test JSONModel loadData [async, Promise(chained)]: multiple requests with merge: 1. request slow",function(assert){ + var done = assert.async(); + var testModel = new JSONModel(); + + testModel.loadData("/fake/testdata3.json"); + testModel.loadData("/fake/testdata4.json"); + testModel.dataLoaded().then(function(oInfo) { + assert.equal(testModel.getProperty("/foo"), "The quick brown fox jumps over the lazy dog."); + assert.equal(testModel.getProperty("/bar"), "ABCDEFGHIJ"); + assert.equal(testModel.getProperty("/baz")[1], 97); + assert.equal(testModel.getProperty("/merged"), true); + done(); + }); + }); + + QUnit.test("test JSONModel loadData [async, Promise]: error during parse",function(assert){ + assert.expect(7); + var done = assert.async(); + var testModel = new JSONModel(); + + var p1 = testModel.loadData("/fake").catch(function(oError) { + assert.equal(oError.message, "parsererror", "parse error leads to rejection - 1"); + assert.equal(oError.responseText, "ERROR!", "parse error leads to rejection - 1"); + }); + + var p2 = testModel.loadData("/fake/broken.json").catch(function(oError) { + assert.equal(oError.message, "parsererror", "parse error leads to rejection - 2"); + assert.equal(oError.responseText, '{"foo": "The quick brown fox jumps over the lazy dog.","bar": "ABCDEFGHIJ""baz": [52, 97]}', "parse error leads to rejection - 2"); + }); + + var p3 = testModel.loadData("/fake/testdata4.json").then(function() { + assert.equal(testModel.getProperty("/foo"), "The quick brown fox jumps over the lazy dog."); + assert.equal(testModel.getProperty("/bar"), "ABCDEFGHIJ"); + assert.equal(testModel.getProperty("/baz")[1], 97); + }); + + Promise.all([p1, p2, p3]).then(function() { + done(); + }); + }); + QUnit.test("test JSONModel loadData: multiple requests with merge: 1. request slow",function(assert){ var done = assert.async(); var testModel = new JSONModel(); diff --git a/src/sap.ui.core/test/sap/ui/core/qunit/json/data/JSONModelFakeService.js b/src/sap.ui.core/test/sap/ui/core/qunit/json/data/JSONModelFakeService.js index 8047f48cc91d..d76bd1e3431c 100644 --- a/src/sap.ui.core/test/sap/ui/core/qunit/json/data/JSONModelFakeService.js +++ b/src/sap.ui.core/test/sap/ui/core/qunit/json/data/JSONModelFakeService.js @@ -39,7 +39,14 @@ sap.ui.define([ '"baz": [52, 97],' + '"merged": true' + '}'; - break; + break; + case "/fake/broken.json": + sAnswer = '{' + + '"foo": "The quick brown fox jumps over the lazy dog.",' + + '"bar": "ABCDEFGHIJ"' + // missing "," + '"baz": [52, 97]' + + '}'; + break; default: // No dummy request!