// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License.txt in the project root for license information. // /// /// // Test Data Source module Helper.ItemsManager { "use strict"; var UI = WinJS.UI; var Scheduler = WinJS.Utilities.Scheduler; interface IFetchResult { items: any[]; offset: number; totalCount?: number; absoluteIndex?: number; atStart?: boolean; atEnd?: boolean; } function createTestDataAdapter(objects, controller, abilities) { // Private members var listDataNotificationHandler, array = [], keyToIndexMap = {}, nextAvailableKey = 0, requests = [], countBeforeDelta, countAfterDelta, countBeforeOverride, countAfterOverride, dataSourceAccessed = false, editsInProgress = false, requestFulfilledSynchronously = false; function directivesForMethod(method, args) { if (controller && controller.directivesForMethod) { return controller.directivesForMethod(method, args); } else { return null; } } function getDataLength() { return typeof controller.returnCount === 'undefined' ? array.length : Math.min(controller.returnCount, array.length); } function item(key, data) { return { key: key.toString(), data: data }; } function setItem(index, key, data) { array[index] = item(key, data); keyToIndexMap[key] = index; } function updateKeyToIndexMap(first) { // Update the key map entries for all indices that changed for (var i = first; i < array.length; i++) { keyToIndexMap[array[i].key] = i; } } function keyFromIndex(index) { return (index < 0 || index >= array.length ? null : array[index].key); } function indexIfSupported(index) { return (abilities && !abilities.itemsFromIndex ? undefined : index); } function itemsFromIndexImplementation(complete, error, index, countBefore, countAfter, returnAbsoluteIndex, maxLength) { if (controller.communicationFailure) { error(new WinJS.ErrorFromName(UI.FetchError.noResponse.toString())); } else { var directives = directivesForMethod("itemsFromIndex", [index, countBefore, countAfter]); dataSourceAccessed = true; countBefore = (countBeforeOverride >= 0 ? countBeforeOverride : Math.max(countBefore + countBeforeDelta, 0)); countAfter = (countAfterOverride >= 0 ? countAfterOverride : Math.max(countAfter + countAfterDelta, 0)); // returnCount simulate a datasource that is changing size. // If returnCount is defined, then the maximum number of items can be retrived from this dataSource is returnCount. if (index >= maxLength) { error(new WinJS.ErrorFromName(UI.FetchError.doesNotExist.toString())); } else { var first = Math.max(0, index - countBefore), last = Math.min(maxLength - 1, index + countAfter), items = new Array(last + 1 - first); for (var i = first; i <= last; i++) { var item = array[i]; items[i - first] = { key: item.key, data: item.data }; } var fetchResult: IFetchResult = { items: items, offset: index - first, totalCount: (directives.omitCount ? undefined : maxLength), // TODO: omitCount is always undefined. This seems a test bug. I }; if (returnAbsoluteIndex && !directives.omitIndices) { fetchResult.absoluteIndex = index; } if (first === 0) { // If the first item is being returned, volunteer this information fetchResult.atStart = true; } if (last >= maxLength) { // If the last item is being returned, volunteer this information fetchResult.atEnd = true; } complete(fetchResult); } } } function keyValid(key, error) { if (typeof keyToIndexMap[key] !== "number") { error(new WinJS.ErrorFromName(UI.EditError.noLongerMeaningful.toString())); return false; } else { return true; } } function noop() { } function insert(complete, error, index, data) { if (controller.communicationFailure) { error(new WinJS.ErrorFromName(UI.EditError.noResponse.toString())); } else { var itemNew = item(nextAvailableKey++, data); array.splice(index, 0, itemNew); updateKeyToIndexMap(index); complete(itemNew); } } function change(complete, error, key, dataNew) { if (controller.communicationFailure) { error(new WinJS.ErrorFromName(UI.EditError.noResponse.toString())); } else if (controller.readOnly) { error(new WinJS.ErrorFromName(UI.EditError.notPermitted.toString())); } else if (controller.notMeaningfulEdit) { error(new WinJS.ErrorFromName(UI.EditError.noLongerMeaningful.toString())); } else { var index = keyToIndexMap[key]; array[index].data = dataNew; complete(); } } function move(complete, error, indexTo, key) { if (controller.communicationFailure) { error(new WinJS.ErrorFromName(UI.EditError.noResponse.toString())); } else { var indexFrom = keyToIndexMap[key], removed = array.splice(indexFrom, 1); if (indexFrom < indexTo) { indexTo--; } array.splice(indexTo, 0, removed[0]); updateKeyToIndexMap(Math.min(indexFrom, indexTo)); complete(); } } function remove(complete, error, key) { if (controller.communicationFailure) { error(new WinJS.ErrorFromName(UI.EditError.noResponse.toString())); } else { var index = keyToIndexMap[key]; delete keyToIndexMap[key]; array.splice(index, 1); updateKeyToIndexMap(index); complete(); } } function insertionIndex(previousKey, nextKey) { var index; if (previousKey === null) { return keyToIndexMap[nextKey]; } index = keyToIndexMap[previousKey] + 1; if (nextKey !== null && keyToIndexMap[nextKey] !== index) { throw new Error("Invalid arguments: previousKey and nextKey should be the keys of adjacent elements."); } return index; } function changeNotificationsEnabled(method, args) { var directives = directivesForMethod(method, args); return directives && directives.sendChangeNotifications; } function implementRequest(request) { var directives = request.directives; countBeforeDelta = 0; countAfterDelta = 0; countBeforeOverride = -1; countAfterOverride = -1; if (directives) { if (directives.countBeforeDelta !== undefined) { countBeforeDelta = directives.countBeforeDelta; } if (directives.countAfterDelta !== undefined) { countAfterDelta = directives.countAfterDelta; } if (directives.countBeforeOverride > 0) { countBeforeOverride = directives.countBeforeOverride; } if (directives.countAfterOverride > 0) { countAfterOverride = directives.countAfterOverride; } } request.execute(); } function createPromise(method, args, promiseInit) { return new WinJS.Promise(function (complete, error) { var directives = directivesForMethod(method, args); var request = { method: method, args: args, directives: directives, execute: function () { promiseInit(complete, error); } }; if (!directives || !directives.callMethodSynchronously) { requests.push(request); if (directives && typeof directives.delay === "number") { var implementNextRequest = function implementNextRequest() { if (requests.length > 0) { implementRequest(requests.splice(0, 1)[0]); } }; if (directives.delay === 0) { Scheduler.schedule(function () { implementNextRequest(); }, Scheduler.Priority.high, null, "TestComponents._TestDataSource._implementNextRequest"); } else { setTimeout(implementNextRequest, directives.delay); } } } else { requestFulfilledSynchronously = true; implementRequest(request); } }); } // Construction controller = controller || {}; // Build the item array and key map for (var i = 0, length = objects.length; i < length; i++) { setItem(i, nextAvailableKey, objects[i]); nextAvailableKey++; } // Public methods return { // DataSource methods setNotificationHandler: function (notificationHandler) { listDataNotificationHandler = notificationHandler; }, itemSignature: abilities && !abilities.itemSignature ? undefined : function (itemData) { if (WinJS.Utilities._isDOMElement(itemData)) { return "<" + WinJS.Utilities._uniqueID(itemData) + ">"; } return "<" + JSON.stringify(itemData) + ">"; }, itemsFromStart: abilities && !abilities.itemsFromStart ? undefined : function (count) { var maxLength = getDataLength(); return createPromise("itemsFromStart", arguments, function (complete, error) { if (typeof controller.returnCountBeforePromise === 'undefined') { maxLength = getDataLength(); } if (maxLength === 0) { error(new WinJS.ErrorFromName(UI.FetchError.doesNotExist.toString())); } else { itemsFromIndexImplementation(complete, error, 0, 0, count - 1, false, maxLength); } }); }, itemsFromEnd: abilities && !abilities.itemsFromEnd ? undefined : function (count) { var maxLength = getDataLength(); return createPromise("itemsFromEnd", arguments, function (complete, error) { if (typeof controller.returnCountBeforePromise === 'undefined') { maxLength = getDataLength(); } if (maxLength === 0) { error(new WinJS.ErrorFromName(UI.FetchError.doesNotExist.toString())); } else { itemsFromIndexImplementation(complete, error, maxLength - 1, count - 1, 0, true, maxLength); } }); }, itemsFromKey: abilities && !abilities.itemsFromKey ? undefined : function (key, countBefore, countAfter) { var maxLength = getDataLength(); return createPromise("itemsFromKey", arguments, function (complete, error) { var index = keyToIndexMap[key]; if (typeof controller.returnCountBeforePromise === 'undefined') { maxLength = getDataLength(); } if (typeof index !== "number") { error(new WinJS.ErrorFromName(UI.FetchError.doesNotExist.toString())); } else { itemsFromIndexImplementation(complete, error, index, countBefore, countAfter, true, maxLength); } }); }, itemsFromIndex: abilities && !abilities.itemsFromIndex ? undefined : function (index, countBefore, countAfter) { var maxLength = getDataLength(); return createPromise("itemsFromIndex", arguments, function (complete, error) { if (typeof controller.returnCountBeforePromise === 'undefined') { maxLength = getDataLength(); } itemsFromIndexImplementation(complete, error, index, countBefore, countAfter, false, maxLength); }); }, itemsFromDescription: abilities && !abilities.itemsFromDescription ? undefined : function (description, countBefore, countAfter) { var maxLength = getDataLength(); return createPromise("itemsFromDescription", arguments, function (complete, error) { // TODO: For now, just treat the description as a key var index = keyToIndexMap[description]; if (typeof controller.returnCountBeforePromise === 'undefined') { maxLength = getDataLength(); } if (typeof index !== "number") { error(new WinJS.ErrorFromName(UI.FetchError.doesNotExist.toString())); } else { itemsFromIndexImplementation(complete, error, index, countBefore, countAfter, true, maxLength); } }); }, getCount: abilities && !abilities.getCount ? undefined : function () { var maxLength = getDataLength(); return createPromise("getCount", arguments, function (complete, error) { if (typeof controller.returnCountBeforePromise === 'undefined') { maxLength = getDataLength(); } if (controller.count_NoResponse) { error(new WinJS.ErrorFromName(UI.CountError.noResponse.toString())); } else if (controller.countUnknown) { error(new WinJS.ErrorFromName(UI.CountResult.unknown.toString())); } else { complete(maxLength); } }); }, beginEdits: abilities && !abilities.beginEdits ? undefined : function () { if (editsInProgress) { throw new Error(); } editsInProgress = true; }, insertAtStart: abilities && !abilities.insertAtStart ? undefined : function (key, data) { return createPromise("insertAtStart", arguments, function (complete, error) { // key parameter is ignored, as keys are generated insert(complete, error, 0, data); }); }, insertBefore: abilities && !abilities.insertBefore ? undefined : function (key, data, nextKey) { return createPromise("insertBefore", arguments, function (complete, error) { // key parameter is ignored, as keys are generated if (keyValid(nextKey, error)) { insert(complete, error, keyToIndexMap[nextKey], data); } }); }, insertAfter: abilities && !abilities.insertAfter ? undefined : function (key, data, previousKey) { return createPromise("insertAfter", arguments, function (complete, error) { // key parameter is ignored, as keys are generated if (keyValid(previousKey, error)) { insert(complete, error, keyToIndexMap[previousKey] + 1, data); } }); }, insertAtEnd: abilities && !abilities.insertAtEnd ? undefined : function (key, data) { var maxLength = getDataLength(); return createPromise("insertAtEnd", arguments, function (complete, error) { // key parameter is ignored, as keys are generated if (typeof controller.returnCountBeforePromise === 'undefined') { maxLength = getDataLength(); } insert(complete, error, maxLength, data); }); }, change: abilities && !abilities.change ? undefined : function (key, newData) { return createPromise("change", arguments, function (complete, error) { if (keyValid(key, error)) { change(complete, error, key, newData); } }); }, moveToStart: abilities && !abilities.moveToStart ? undefined : function (key) { return createPromise("moveToStart", arguments, function (complete, error) { if (keyValid(key, error)) { move(complete, error, 0, key); } }); }, moveBefore: abilities && !abilities.moveBefore ? undefined : function (key, nextKey) { return createPromise("moveBefore", arguments, function (complete, error) { if (keyValid(key, error) && keyValid(nextKey, error)) { move(complete, error, keyToIndexMap[nextKey], key); } }); }, moveAfter: abilities && !abilities.moveAfter ? undefined : function (key, previousKey) { return createPromise("moveAfter", arguments, function (complete, error) { if (keyValid(key, error) && keyValid(previousKey, error)) { move(complete, error, keyToIndexMap[previousKey] + 1, key); } }); }, moveToEnd: abilities && !abilities.moveToEnd ? undefined : function (key) { var maxLength = getDataLength(); return createPromise("moveToEnd", arguments, function (complete, error) { if (typeof controller.returnCountBeforePromise === 'undefined') { maxLength = getDataLength(); } if (keyValid(key, error)) { move(complete, error, maxLength, key); } }); }, remove: abilities && !abilities.remove ? undefined : function (key) { return createPromise("remove", arguments, function (complete, error) { if (keyValid(key, error)) { remove(complete, error, key); } }); }, endEdits: abilities && !abilities.endEdits ? undefined : function () { if (!editsInProgress) { throw new Error(); } editsInProgress = false; }, // Test methods // Returns true if the data source has been accessed since the last call to clearDataSourceAccessed isDataSourceAccessed: function () { return dataSourceAccessed; }, clearDataSourceAccessed: function () { dataSourceAccessed = false; }, insertItem: function (key, data, previousKey, nextKey) { // key parameter is ignored, as keys are generated var index = insertionIndex(previousKey, nextKey); array.splice(index, 0, item(nextAvailableKey, data)); updateKeyToIndexMap(index); nextAvailableKey++; }, changeItem: function (key, newData) { array[keyToIndexMap[key]].data = newData; }, removeItem: function (key) { var index = keyToIndexMap[key]; delete keyToIndexMap[key]; array.splice(index, 1); updateKeyToIndexMap(index); }, moveItem: function (key, previousKey, nextKey) { var indexFrom = keyToIndexMap[key], indexTo = insertionIndex(previousKey, nextKey), removed = array.splice(indexFrom, 1); if (indexFrom < indexTo) { indexTo--; } array.splice(indexTo, 0, removed[0]); updateKeyToIndexMap(Math.min(indexFrom, indexTo)); }, // Sets the value for the dataSourceAccessed flag setDataSourceAccessedFlag: function (status) { dataSourceAccessed = status; }, // Call to this method will resume data source edits reEstablishDSConnection: function () { controller.communicationFailure = false; }, // This will set properties like readOnly/notMeaningfulEdit. // Based on these values, DSA returns notPermitted/noLongerMeaningful status code from the editing methods. setProperty: function (propertyName, value) { if (propertyName === "notMeaningfulEdit") controller.notMeaningfulEdit = value; else if (propertyName === "readOnly") controller.readOnly = value; else if (propertyName === "communicationFailure") controller.communicationFailure = value; else if (propertyName === "count_NoResponse") controller.count_NoResponse = true; else if (propertyName === "countUnknown") controller.countUnknown = true; else if (propertyName === "returnCount") controller.returnCount = value; else if (propertyName === "returnCountBeforePromise") // when value is defined, returnCount is calcualted before promise created; otherwise calculated when promise executed. // By default, returnCountBeforePromise is undefined; and all the previous test is using the value calculated when promise executed and the value is array.length // Thus, adding this varible does not break existing test. // This value should be set at beginning of a test; and should not be changed in the test. // If you change it during a test, then the promise still in process will use the new value. controller.returnCountBeforePromise = value; }, swapItems: function (indexA, indexB) { var temp = array[indexA]; array[indexA] = array[indexB]; array[indexB] = temp; updateKeyToIndexMap(0); }, changeKey: function (oldKey, newKey) { var index = keyToIndexMap[oldKey]; array[index].key = newKey; delete keyToIndexMap[oldKey]; keyToIndexMap[newKey] = index; }, requestCount: function () { return requests.length; }, readRequest: function (index) { return requests[index]; }, fulfillRequest: function (index) { implementRequest(requests.splice(index, 1)[0]); }, fulfillNextRequest: function () { if (requests.length > 0) { this.fulfillRequest(0); } }, fulfillRandomRequest: function () { if (requests.length > 0) { this.fulfillRequest(Helper.ItemsManager.pseudorandom(requests.length)); } }, fulfillAllRequests: function () { while (requests.length > 0) { this.fulfillNextRequest(); } }, requestFulfilledSynchronously: function () { var result = requestFulfilledSynchronously; // Calling this method clears the flag requestFulfilledSynchronously = false; return result; }, replaceItems: function (items) { array = []; keyToIndexMap = {}; for (var i = 0, length = items.length; i < length; i++) { var item = items[i]; setItem(i, item.key, item.data) var keyValue = parseInt(item.key, 10); if (nextAvailableKey <= keyValue) { nextAvailableKey = keyValue + 1; } } }, getItems: function () { return array; }, invalidateAll: function () { listDataNotificationHandler.invalidateAll(); }, reload: function () { listDataNotificationHandler.reload(); }, indexFromKey: function (key) { return keyToIndexMap[key]; }, currentCount: function () { return array.length; }, currentMaxLength: function () { var maxLength = getDataLength(); return maxLength; }, beginModifications: function () { if (changeNotificationsEnabled("beginNotifications", arguments)) { listDataNotificationHandler.beginNotifications(); } }, insertAtIndex: function (data, index) { LiveUnit.Assert.isTrue(index >= 0 && index <= array.length); insert(noop, noop, index, data); if (changeNotificationsEnabled("insertAtIndex", arguments)) { listDataNotificationHandler.inserted(array[index], keyFromIndex(index - 1), keyFromIndex(index + 1), indexIfSupported(index)); } }, changeAtIndex: function (index, newData) { LiveUnit.Assert.isTrue(index >= 0 && index < array.length); var key = array[index].key; change(noop, noop, key, newData); if (changeNotificationsEnabled("changeAtIndex", arguments)) { listDataNotificationHandler.changed(array[index]); } }, moveToIndex: function (index, newIndex) { LiveUnit.Assert.isTrue(index >= 0 && index < array.length); LiveUnit.Assert.isTrue(newIndex >= 0 && newIndex <= array.length); var key = array[index].key, data = array[index].data, previousKey = keyFromIndex(newIndex - 1), nextKey = keyFromIndex(newIndex); move(noop, noop, newIndex, key); if (changeNotificationsEnabled("moveToIndex", arguments)) { listDataNotificationHandler.moved(array[keyToIndexMap[key]], previousKey, nextKey, indexIfSupported(index), indexIfSupported(newIndex)); } }, removeAtIndex: function (index) { LiveUnit.Assert.isTrue(index >= 0 && index < array.length); var key = array[index].key; remove(noop, noop, key); if (changeNotificationsEnabled("removeAtIndex", arguments)) { listDataNotificationHandler.removed(key, indexIfSupported(index)); } }, endModifications: function () { if (changeNotificationsEnabled("endModifications", arguments)) { listDataNotificationHandler.endNotifications(); } } }; }; var TestDataSource = WinJS.Class.derive(UI.VirtualizedDataSource, function (objects, controller, abilities) { var testDataAdapter = createTestDataAdapter(objects, controller, abilities); this._baseDataSourceConstructor(testDataAdapter); this.testDataAdapter = testDataAdapter; }); export function createTestDataSource(objects, controller, abilities) { return new TestDataSource(objects, controller, abilities); }; }