// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License.txt in the project root for license information. // /// /// /// module WinJSTests { "use strict"; var previousTracingOptions; var step: number; var iteration: number; function errorHandler(e) { // Currently we're just using this to suppress application termination. } function testSimpleNotifications(signalTestCaseCompleted, synchronous) { var dataSource = Helper.ItemsManager.simpleAsynchronousDataSource(0), handler = Helper.ItemsManager.simpleListNotificationHandler(), listBinding = dataSource.createListBinding(handler); if (synchronous) { dataSource.testDataAdapter.directives.callMethodsSynchronously = true; } else { Helper.ItemsManager.ensureAllAsynchronousRequestsFulfilled(dataSource); } var state0 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; Helper.ItemsManager.setState(dataSource, state0); var promises = []; // Fetch all 10 items var itemPromise = listBinding.first(); for (var i = 0; i < 10; i++) { handler.appendItemPromise(itemPromise); (function (i) { promises.push(itemPromise.then(function (item) { handler.updateItem(item); handler.verifyItem(item, i); })); })(i); itemPromise = listBinding.next(); } itemPromise.then(function (item) { LiveUnit.Assert.isNull(item, "Request for item after last item did not return null"); }); WinJS.Promise.join(promises).then(function () { handler.verifyExpectedNotifications([ "beginNotifications", "countChanged", "endNotifications" ]); handler.verifyState(state0, dataSource); // Change the state of the data source, as if it had been changed by an external influence var state1 = [0, 1, 2, 7, 3, 4, 5, 6, 8, 9]; Helper.ItemsManager.setState(dataSource, state1); // Force a refresh and wait for it to complete dataSource.invalidateAll().then(function () { handler.verifyExpectedNotifications([ "beginNotifications", "moved", "indexChanged", "indexChanged", "indexChanged", "indexChanged", "indexChanged", "endNotifications" ]); handler.verifyState(state1, dataSource); // Try three moves and and two insertions var state2 = [9, 10, 0, 1, 2, 7, 3, 6, 8, 11, 4, 5]; Helper.ItemsManager.setState(dataSource, state2); // Force a refresh and wait for it to complete dataSource.invalidateAll().then(function () { handler.verifyExpectedNotifications([ "beginNotifications", "moved", "moved", "moved", "inserted", "inserted", "indexChanged", "indexChanged", "indexChanged", "indexChanged", "indexChanged", "indexChanged", "indexChanged", "indexChanged", "countChanged", "endNotifications" ]); handler.verifyState(state2, dataSource); // Try a move between two insertions and three deletions var state3 = [10, 0, 1, 7, 3, 12, 2, 13, 8, 11, 4]; Helper.ItemsManager.setState(dataSource, state3); // Force a refresh and wait for it to complete dataSource.invalidateAll().then(function () { handler.verifyExpectedNotifications([ "beginNotifications", "removed", "removed", "removed", "moved", "inserted", "inserted", "indexChanged", "indexChanged", "indexChanged", "indexChanged", "indexChanged", "indexChanged", "countChanged", "endNotifications" ]); handler.verifyState(state3, dataSource); signalTestCaseCompleted(); }); }); }); }); } function testInsertAfterClearNotifications(signalTestCaseCompleted, synchronous) { var dataSource = Helper.ItemsManager.simpleAsynchronousDataSource(5), handler = Helper.ItemsManager.simpleListNotificationHandler(), listBinding = dataSource.createListBinding(handler); if (synchronous) { dataSource.testDataAdapter.directives.callMethodsSynchronously = true; } else { Helper.ItemsManager.ensureAllAsynchronousRequestsFulfilled(dataSource); } // Clear the data source var state0 = []; Helper.ItemsManager.setState(dataSource, state0); dataSource.invalidateAll().then(function () { handler.verifyExpectedNotifications([ "beginNotifications", "countChanged", "endNotifications" ]); handler.verifyState(state0, dataSource); // Append three items to the empty data source var state1 = [0, 1, 2]; Helper.ItemsManager.setState(dataSource, state1); dataSource.invalidateAll().then(function () { handler.verifyExpectedNotifications([ "beginNotifications", "countChanged", "endNotifications" ]); var promises = []; // Fetch all the items so that we can verify the state var itemPromise = listBinding.first(); for (var i = 0; ; i++) { handler.appendItemPromise(itemPromise); (function (i) { promises.push(itemPromise.then(function (item) { handler.updateItem(item); handler.verifyItem(item, i); })); })(i); if (i === state1.length - 1) { break; } itemPromise = listBinding.next(); } WinJS.Promise.join(promises).then(function () { handler.verifyState(state1, dataSource); signalTestCaseCompleted(); }); }); }); } var rand = Helper.ItemsManager.pseudorandom; function occurs(probability) { var granularity = 1000; return rand(granularity) < probability * granularity; } function createTestListBinding(dataSource) { var handler = Helper.ItemsManager.stressListNotificationHandler(), listBinding = dataSource.createListBinding(handler); // Add handler as expando on the ListBinding listBinding.handler = handler; return listBinding; } // Stress tests the VirtualizedDataSource by attempting various random operations // // The following behaviors are currently not configurable: // - Three ListBindings are used to read the data, one by index, one by key/description, and one using index/key // - Random contiguous ranges of items are read using the ListBindings // - Some individual items are read directly from the data source // - Random ranges of items are released // - countBefore/countAfter are often overridden with a random value // - Some random edits are made to the data // - invalidateAll is called periodically // - Requests of the data source are sometimes completed out of order // // The following parameters control the data source behaviors. Each is a probability, so a value of 0.0 means the // given behavior never occurs, while 1.0 means it always occurs. Values in between specify frequencies that are // interpreted differently depending on the behavior. // - indices: Are the index or count parameters returned with FetchResults? (Occasionally switches "modes".) // - asynchronous: Does a given request completely asynchronously? // - failures: Is the data source connection lost occasionally? // - changes: Is the data changing independently? // - notifications: Does the data source send synchronous change notifications? function testRandomUsageOnce(indices, asynchronous, failures, changes, notifications, complete) { var count = 300, dataSource = Helper.ItemsManager.simpleAsynchronousDataSource(count), testDataAdapter = dataSource.testDataAdapter, listBinding1 = createTestListBinding(dataSource), listBinding2 = createTestListBinding(dataSource), listBinding3 = createTestListBinding(dataSource); // Probability constants var read1 = 0.30, read2 = 0.10, read3 = 0.20, readDirect = 0.03, resultsCountOverride = 0.40, edit = 0.05, refreshDefault = 0.01, refreshAfterFailure = 0.20, outOfOrder = 0.1; // Other constants var walkMax = 10, countBeforeAfterMax = 20, countEditMax = 5, countToIndicesSwitchMax = 100, countToCountSwitchMax = 100, changeCountMax = 10, changeSizeMax = 30; function localWalk(listBinding, itemPromise, index?) { listBinding.handler.requestItem(itemPromise, index); var handlePrev = itemPromise.handle; // Walk a small distance in each direction var j; var countBefore = rand(walkMax); for (j = 0; j < countBefore; j++) { var itemPromisePrevious = listBinding.previous(); listBinding.handler.requestItem(itemPromisePrevious, index - 1 - j, null, handlePrev); handlePrev = itemPromisePrevious.handle; } listBinding.jumpToItem(itemPromise); handlePrev = itemPromise.handle; var countAfter = rand(walkMax); for (j = 0; j < countAfter; j++) { var itemPromiseNext = listBinding.next(); listBinding.handler.requestItem(itemPromiseNext, index + 1 + j, handlePrev, null); handlePrev = itemPromiseNext.handle; } } var newItemID = 0; function newData(change?) { return "New item " + newItemID++ + (change ? " (change)" : " (edit)"); } var changedItemID = 0; function changedData(change?) { return "Changed item " + changedItemID++ + (change ? " (change)" : " (edit)"); } function randomIndex(changeSize?) { return rand(testDataAdapter.currentCount() - (changeSize ? changeSize - 1 : 0)); } function randomKey() { return "" + rand(count); } var indicesProvided, countToIndicesSwitch = 0, countProvided, countToCountSwitch = 0; function setBehaviors() { var directives = testDataAdapter.directives; // Does the data source return countBefore/countAfter other than those requested? var override = occurs(resultsCountOverride); directives.countBeforeOverride = (override ? rand(countBeforeAfterMax) : -1); directives.countAfterOverride = (override ? rand(countBeforeAfterMax) : -1); // Periodically switch between providing and not providing indices if (countToIndicesSwitch-- === 0) { indicesProvided = occurs(indices); countToIndicesSwitch = rand(countToIndicesSwitchMax); } directives.returnIndices = indicesProvided; // Periodically switch between providing and not providing the count if (countToCountSwitch-- === 0) { countProvided = occurs(indices); // Use the same probability as for indices countToIndicesSwitch = rand(countToCountSwitchMax); } directives.returnCount = countProvided; // Does the request complete asynchronously? directives.callMethodsSynchronously = !occurs(asynchronous); directives.sendChangeNotifications = occurs(notifications); } var refresh = refreshDefault; dataSource.addEventListener("statuschanged", function (ev) { // In the event of a failure, increase the probability of a refresh until one happens if (ev.detail === WinJS.UI.DataSourceStatus.failure) { refresh = refreshAfterFailure; } }); // Run a number of simulation "steps" in each of which the data source fulfills one outstanding request (if // there are any) and the clients make various requests at random times. var stepCount = 70; //5000; step = 0; (function stepOnce() { var index; // Does the next request we fulfill time out? testDataAdapter.directives.communicationFailure = occurs(failures); // Fulfill one request if (occurs(outOfOrder)) { testDataAdapter.fulfillRandomRequest(); } else { testDataAdapter.fulfillNextRequest(); } // The first binding reads by index only if (occurs(read1)) { index = rand(count); setBehaviors(); localWalk(listBinding1, listBinding1.fromIndex(index), index); } // The second binding reads by key or description if (occurs(read2)) { index = rand(count); setBehaviors(); localWalk(listBinding2, rand(2) ? listBinding2.fromKey("" + index) : listBinding2.fromDescription("" + index)); } // The third binding reads by index or key if (occurs(read3)) { index = rand(count); setBehaviors(); localWalk(listBinding3, rand(2) ? listBinding3.fromIndex(index) : listBinding3.fromKey("" + index)); } // Direct reads are by index, key or description if (occurs(readDirect)) { index = rand(count); setBehaviors(); // This test won't pay attention to the results of these fetches switch (rand(3)) { case 0: dataSource.itemFromIndex(index); break; case 1: dataSource.itemFromKey("" + index); break; case 2: dataSource.itemFromDescription("" + index); break; } } // Release items using the same read probabilities if (occurs(read1)) { listBinding1.handler.releaseSomeItems(); } if (occurs(read2)) { listBinding2.handler.releaseSomeItems(); } if (occurs(read3)) { listBinding3.handler.releaseSomeItems(); } // Edits can be made to any item, whether or not it has already been requested if (occurs(edit)) { var editCount = rand(countEditMax); // Just use the original keys for the edits; some will have been removed, so some of these edits will // fail and be undone for (var i = 0; i < editCount; i++) { switch (rand(10)) { case 0: dataSource.insertAtStart(null, newData()).then(null, errorHandler); break; case 1: dataSource.insertBefore(null, newData(), randomKey()).then(null, errorHandler); break; case 2: dataSource.insertAfter(null, newData(), randomKey()).then(null, errorHandler); break; case 3: dataSource.insertAtEnd(null, newData()).then(null, errorHandler); break; case 4: dataSource.change(randomKey(), changedData()).then(null, errorHandler); break; case 5: dataSource.moveToStart(randomKey()).then(null, errorHandler); break; case 6: dataSource.moveBefore(randomKey(), randomKey()).then(null, errorHandler); break; case 7: dataSource.moveAfter(randomKey(), randomKey()).then(null, errorHandler); break; case 8: dataSource.moveToEnd(randomKey()).then(null, errorHandler); break; case 9: dataSource.remove(randomKey()).then(null, errorHandler); break; } } } // Sometimes cause a refresh manually if (occurs(refresh)) { dataSource.invalidateAll(); refresh = refreshDefault; } // Does the underlying data change? if (occurs(changes)) { var changeCount = rand(changeCountMax); testDataAdapter.directives.sendChangeNotifications = false; for (var i = 0; i < changeCount; i++) { // Half the time, a single item is moved var changeSize = rand(2) ? 1 : rand(changeSizeMax), indexSource = randomIndex(changeSize), indexDestination = rand(testDataAdapter.currentCount() + 1), operation = rand(4); for (var j = 0; j < changeSize; j++) { switch (operation) { case 0: testDataAdapter.insertAtIndex(newData(true), indexDestination + j); break; case 1: testDataAdapter.changeAtIndex(indexSource + j, changedData(true)); break; case 2: testDataAdapter.moveToIndex(indexSource, indexDestination + (indexSource < indexDestination ? 0 : j)); break; case 3: testDataAdapter.removeAtIndex(indexSource); break; } } } } listBinding1.handler.verifyIntermediateState(); listBinding2.handler.verifyIntermediateState(); listBinding3.handler.verifyIntermediateState(); var extraIterations = 0; // See if it's time to wrap up and wait for everything to complete if (++step < stepCount) { WinJS.Utilities._setImmediate(stepOnce); } else { // Refresh one last time in case there were errors and fetching stopped dataSource.invalidateAll(); // Rather than adding a then handler to invalidateAll, enter a loop to keep fulfilling requests until // the requests have clearly stopped. (function completeAllRequests() { if (testDataAdapter.requestCount() === 0 && !testDataAdapter.requestFulfilledSynchronously()) { extraIterations++; } else { extraIterations = 0; } if (extraIterations === 3) { var itemArray = testDataAdapter.getItems(); listBinding1.handler.verifyFinalState(itemArray); listBinding2.handler.verifyFinalState(itemArray); listBinding3.handler.verifyFinalState(itemArray); complete(); } else { testDataAdapter.fulfillAllRequests(); WinJS.Utilities._setImmediate(completeAllRequests); } })(); } })(); // REVIEW: With groups, no edits, and be careful to keep groups contiguous while inserting/removing in underlying data // (Though GroupDataSource probably needs to tolerate non-contiguous groups, as we can hit that state briefly.) } function testRandomUsage(indices, asynchronous, failures, changes, notifications, signalTestCaseCompleted) { var iterationCount = 5 * Helper.ItemsManager.stressLevel; iteration = 0; (function continueTest() { LiveUnit.LoggingCore.logComment("Test " + iteration + " of " + iterationCount); Helper.ItemsManager.seedPseudorandom(iteration); testRandomUsageOnce(indices, asynchronous, failures, changes, notifications, function () { if (++iteration < iterationCount) { WinJS.Utilities._setImmediate(continueTest); } else { signalTestCaseCompleted(); } }); })(); } // verify the newCount and oldCount returned in countChanged handler are correct. No difference is expected in sync/async mode although testing both of them. function testCountChangedNotificationWithChangingDataSource(signalTestCaseCompleted, synchronous) { var dataSource = Helper.ItemsManager.simpleAsynchronousDataSource(5), handler = Helper.ItemsManager.simpleListNotificationHandler(), listBinding = dataSource.createListBinding(handler); var newCountValue = 0; var oldCountValue: any = 0; var testCount = 2; // number of countChanged should be called. Using this variable to terminate this test. handler.countChanged = function (newCount, oldCount) { this.queueNotification("countChanged", arguments); LiveUnit.Assert.areEqual(newCount, newCountValue, "Expecting: " + newCountValue.toString() + "; Actual: " + newCount.toString()); LiveUnit.Assert.areEqual(oldCount, oldCountValue, "Expecting: " + oldCountValue.toString() + "; Actual: " + oldCount.toString()); testCount--; if (testCount === 0) { signalTestCaseCompleted(); } }; if (synchronous) { dataSource.testDataAdapter.directives.callMethodsSynchronously = true; } else { Helper.ItemsManager.ensureAllAsynchronousRequestsFulfilled(dataSource); } var state0 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; Helper.ItemsManager.setState(dataSource, state0); newCountValue = 10; oldCountValue = "unknown"; listBinding.first().then(function () { var state1 = [0, 1, 2, 3, 4]; Helper.ItemsManager.setState(dataSource, state1); oldCountValue = newCountValue; newCountValue = 5; return dataSource.invalidateAll(); }) .then(function () { var state2 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; Helper.ItemsManager.setState(dataSource, state2); oldCountValue = newCountValue; newCountValue = 12; return dataSource.invalidateAll(); }).done(); } export class NotificationTests { setUp() { previousTracingOptions = VDSLogging.options; VDSLogging.options = { log: function (message) { LiveUnit.Assert.fail(message); }, include: /createListBinding|_retainItem|_releaseItem|release/, handleTracking: true, logVDS: true, stackTraceLimit: 0 // set this to 100 to get good stack traces if you run into a failure. }; VDSLogging.on(); } tearDown() { VDSLogging.off(); VDSLogging.options = previousTracingOptions; } testEmptyListNotifications(signalTestCaseCompleted) { var dataSource = Helper.ItemsManager.simpleAsynchronousDataSource(0), handler = Helper.ItemsManager.simpleListNotificationHandler(), listBinding = dataSource.createListBinding(handler); Helper.ItemsManager.ensureAllAsynchronousRequestsFulfilled(dataSource); // Fetch the first item, which should return a placeholder var itemPromise = listBinding.first(); handler.appendItemPromise(itemPromise); // Wait for the fetch to discover that the item doesn't exist itemPromise.then(function (item) { LiveUnit.Assert.isNull(item, "First item in list known to be empty not null"); Helper.ItemsManager.verifyRequestCount(dataSource, 0); // This will actually be called from the refresh - wait for it to complete Helper.ItemsManager.setImmediate(function () { Helper.ItemsManager.verifyRequestCount(dataSource, 0); // The notifications will have been sent after the promise completed, so verify them now handler.verifyExpectedNotifications([ "beginNotifications", "removed", "countChanged", "endNotifications" ]); // Fetch the last item, which should return null synchronously var synchronous = false; listBinding.last().then(function (item2) { synchronous = true; LiveUnit.Assert.isNull(item2, "Last item in list known to be empty not null"); }); LiveUnit.Assert.isTrue(synchronous, "Fetching last item did not complete synchronously"); // Fetch the first item again, which should return null synchronously this time synchronous = false; listBinding.first().then(function (item2) { synchronous = true; LiveUnit.Assert.isNull(item2, "First item in list known to be empty not null"); }); LiveUnit.Assert.isTrue(synchronous, "Fetching first item did not complete synchronously"); // We don't expect there to be any outstanding requests, or to have received any notifications Helper.ItemsManager.verifyRequestCount(dataSource, 0); handler.verifyExpectedNotifications([]); signalTestCaseCompleted(); }); }); } testSimpleNotificationsAsynchronous(signalTestCaseCompleted) { testSimpleNotifications(signalTestCaseCompleted, false); } testSimpleNotificationsSynchronous(signalTestCaseCompleted) { testSimpleNotifications(signalTestCaseCompleted, true); } testInsertNotifications(signalTestCaseCompleted) { var dataSource = Helper.ItemsManager.simpleAsynchronousDataSource(5), handler = Helper.ItemsManager.simpleListNotificationHandler(), listBinding = dataSource.createListBinding(handler); dataSource.testDataAdapter.directives.callMethodsSynchronously = true; var state0 = [0, 1, 2, 3, 4]; Helper.ItemsManager.setState(dataSource, state0); var promises = []; // Fetch just the first three items var itemPromise = listBinding.first(); for (var i = 0; ; i++) { handler.appendItemPromise(itemPromise); (function (i) { promises.push(itemPromise.then(function (item) { handler.updateItem(item); handler.verifyItem(item, i); })); })(i); if (i === 2) { break; } itemPromise = listBinding.next(); } WinJS.Promise.join(promises).then(function () { handler.verifyExpectedNotifications([ "beginNotifications", "countChanged", "endNotifications" ]); // Append three items directly to the data source var state1 = [0, 1, 2, 3, 4, 5, 6, 7]; Helper.ItemsManager.setState(dataSource, state1); // Force a refresh and wait for it to complete dataSource.invalidateAll().then(function () { // We should have received countChanged, but no inserted notifications handler.verifyExpectedNotifications([ "beginNotifications", "countChanged", "endNotifications" ]); // Don't verify state here, since only a portion of the list has been read signalTestCaseCompleted(); }); }); } testInsertAtEndNotifications(signalTestCaseCompleted) { var dataSource = Helper.ItemsManager.simpleAsynchronousDataSource(5), handler = Helper.ItemsManager.simpleListNotificationHandler(), listBinding = dataSource.createListBinding(handler); dataSource.testDataAdapter.directives.callMethodsSynchronously = true; var state0 = [0, 1, 2, 3, 4]; Helper.ItemsManager.setState(dataSource, state0); var promises = []; // Fetch all the items var itemPromise = listBinding.first(); for (var i = 0; ; i++) { handler.appendItemPromise(itemPromise); (function (i) { promises.push(itemPromise.then(function (item) { handler.updateItem(item); handler.verifyItem(item, i); })); })(i); if (i === state0.length - 1) { break; } itemPromise = listBinding.next(); } WinJS.Promise.join(promises).then(function () { handler.verifyExpectedNotifications([ "beginNotifications", "countChanged", "endNotifications" ]); // Append three items directly to the data source var state1 = [0, 1, 2, 3, 4, 5, 6, 7]; Helper.ItemsManager.setState(dataSource, state1); // Force a refresh and wait for it to complete dataSource.invalidateAll().then(function () { // Because we'd "observed" the end of the list, we should have received three inserted notifications, // plus countChanged. handler.verifyExpectedNotifications([ "beginNotifications", "inserted", "inserted", "inserted", "countChanged", "endNotifications" ]); // Don't verify state here, since only a portion of the list has been read signalTestCaseCompleted(); }); }); } testInsertAfterClearNotificationsAsynchronous(signalTestCaseCompleted) { testInsertAfterClearNotifications(signalTestCaseCompleted, false); } testInsertAfterClearNotificationsSynchronous(signalTestCaseCompleted) { testInsertAfterClearNotifications(signalTestCaseCompleted, true); } testRefreshWithTwoListBindings(signalTestCaseCompleted) { var dataSource = Helper.ItemsManager.simpleAsynchronousDataSource(0), handler1 = Helper.ItemsManager.simpleListNotificationHandler(), listBinding1 = dataSource.createListBinding(handler1), handler2 = Helper.ItemsManager.simpleListNotificationHandler(), listBinding2 = dataSource.createListBinding(handler2); Helper.ItemsManager.ensureAllAsynchronousRequestsFulfilled(dataSource); var state0 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; Helper.ItemsManager.setState(dataSource, state0); var promises = []; // Fetch the first three items using one ListBinding var itemPromise = listBinding1.first(); for (var i = 0; ; i++) { handler1.appendItemPromise(itemPromise); (function (i) { promises.push(itemPromise.then(function (item) { handler1.updateItem(item); handler1.verifyItem(item, i); })); })(i); if (i === 2) { break; } itemPromise = listBinding1.next(); } WinJS.Promise.join(promises).then(function () { handler1.verifyExpectedNotifications([ "beginNotifications", "countChanged", "endNotifications" ]); handler2.verifyExpectedNotifications([ "beginNotifications", "countChanged", "endNotifications" ]); // Now have the other ListBinding request some items by key handler2.appendItemPromise(listBinding2.fromKey("2")); handler2.appendItemPromise(listBinding2.fromKey("7")); // Before the data source gets a chance to respond, cause a refresh dataSource.invalidateAll().then(function () { // Since there were no mirages and nothing actually changed in the data, the only notification received // should be an indexChanged when the index for item "7" was determined. handler1.verifyExpectedNotifications([]); handler2.verifyExpectedNotifications([ "beginNotifications", "indexChanged", "endNotifications" ]); signalTestCaseCompleted(); }); }); } testRandomUsageWithoutIndependentChanges(signalTestCaseCompleted) { testRandomUsage( 0.30, // indices 0.80, // asynchronous 0.02, // failures 0.00, // changes 0.00, // notifications signalTestCaseCompleted ); } xtestRandomUsageWithoutNotifications(signalTestCaseCompleted) { testRandomUsage( 0.30, // indices 0.80, // asynchronous 0.02, // failures 0.20, // changes 0.00, // notifications signalTestCaseCompleted ); } xtestRandomUsageWithNotifications(signalTestCaseCompleted) { testRandomUsage( 0.30, // indices 0.80, // asynchronous 0.02, // failures 0.20, // changes 0.30, // notifications signalTestCaseCompleted ); } testCountChangedNotificationWithChangingDataSourceAsynchronous(signalTestCaseCompleted) { testCountChangedNotificationWithChangingDataSource(signalTestCaseCompleted, false); } testCountChangedNotificationWithChangingDataSourceSynchronous(signalTestCaseCompleted) { testCountChangedNotificationWithChangingDataSource(signalTestCaseCompleted, true); } }; } // Register the object as a test class by passing in the name LiveUnit.registerTestClass("WinJSTests.NotificationTests");