diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js
index 50fff01fb012..1f970369cd3d 100644
--- a/src/ngMock/angular-mocks.js
+++ b/src/ngMock/angular-mocks.js
@@ -37,10 +37,26 @@ angular.mock.$Browser = function() {
self.$$lastUrl = self.$$url; // used by url polling fn
self.pollFns = [];
- // TODO(vojta): remove this temporary api
- self.$$completeOutstandingRequest = angular.noop;
- self.$$incOutstandingRequestCount = angular.noop;
-
+ // Testability API
+
+ var outstandingRequestCount = 0;
+ var outstandingRequestCallbacks = [];
+ self.$$incOutstandingRequestCount = function() { outstandingRequestCount++; };
+ self.$$completeOutstandingRequest = function() {
+ outstandingRequestCount--;
+ if (!outstandingRequestCount) {
+ while (outstandingRequestCallbacks.length) {
+ outstandingRequestCallbacks.pop()();
+ }
+ }
+ };
+ self.notifyWhenNoOutstandingRequests = function(callback) {
+ if (outstandingRequestCount) {
+ outstandingRequestCallbacks.push(callback);
+ } else {
+ callback();
+ }
+ };
// register url polling fn
@@ -166,10 +182,6 @@ angular.mock.$Browser.prototype = {
state: function() {
return this.$$state;
- },
-
- notifyWhenNoOutstandingRequests: function(fn) {
- fn();
}
};
diff --git a/src/ngRoute/route.js b/src/ngRoute/route.js
index c3b78776e863..3f18d8cbbc7c 100644
--- a/src/ngRoute/route.js
+++ b/src/ngRoute/route.js
@@ -7,6 +7,7 @@
var isArray;
var isObject;
var isDefined;
+var noop;
/**
* @ngdoc module
@@ -54,6 +55,7 @@ function $RouteProvider() {
isArray = angular.isArray;
isObject = angular.isObject;
isDefined = angular.isDefined;
+ noop = angular.noop;
function inherit(parent, extra) {
return angular.extend(Object.create(parent), extra);
@@ -350,7 +352,8 @@ function $RouteProvider() {
'$injector',
'$templateRequest',
'$sce',
- function($rootScope, $location, $routeParams, $q, $injector, $templateRequest, $sce) {
+ '$browser',
+ function($rootScope, $location, $routeParams, $q, $injector, $templateRequest, $sce, $browser) {
/**
* @ngdoc service
@@ -677,9 +680,10 @@ function $RouteProvider() {
} else if (nextRoute || lastRoute) {
forceReload = false;
$route.current = nextRoute;
-
var nextRoutePromise = $q.resolve(nextRoute);
+ $browser.$$incOutstandingRequestCount();
+
nextRoutePromise.
then(getRedirectionData).
then(handlePossibleRedirection).
@@ -693,17 +697,24 @@ function $RouteProvider() {
nextRoute.locals = locals;
angular.copy(nextRoute.params, $routeParams);
}
+
$rootScope.$broadcast('$routeChangeSuccess', nextRoute, lastRoute);
}
});
- }).catch(function(error) {
+ }).
+ catch(function(error) {
if (nextRoute === $route.current) {
$rootScope.$broadcast('$routeChangeError', nextRoute, lastRoute, error);
}
- });
+ }).
+ finally(decrementRequestCount);
}
}
+ function decrementRequestCount() {
+ $browser.$$completeOutstandingRequest(noop);
+ }
+
function getRedirectionData(route) {
var data = {
route: route,
diff --git a/test/e2e/fixtures/ng-route-promise/index.html b/test/e2e/fixtures/ng-route-promise/index.html
new file mode 100644
index 000000000000..ef6fe281ab8c
--- /dev/null
+++ b/test/e2e/fixtures/ng-route-promise/index.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/test/e2e/fixtures/ng-route-promise/script.js b/test/e2e/fixtures/ng-route-promise/script.js
new file mode 100644
index 000000000000..bc9b75b51e8a
--- /dev/null
+++ b/test/e2e/fixtures/ng-route-promise/script.js
@@ -0,0 +1,25 @@
+'use strict';
+
+angular.
+ module('lettersApp', ['ngRoute']).
+ config(function($routeProvider) {
+ $routeProvider.
+ when('/foo', {
+ resolveRedirectTo: function($q) {
+ return $q(function(resolve) {
+ window.setTimeout(resolve, 1000, '/bar');
+ });
+ }
+ }).
+ when('/bar', {
+ template: '',
+ resolve: {
+ letters: function($q) {
+ return $q(function(resolve) {
+ window.setTimeout(resolve, 1000, ['a', 'b', 'c', 'd', 'e']);
+ });
+ }
+ }
+ }).
+ otherwise('/foo');
+ });
diff --git a/test/e2e/tests/ng-route-promise.spec.js b/test/e2e/tests/ng-route-promise.spec.js
new file mode 100644
index 000000000000..eb47d9cb801b
--- /dev/null
+++ b/test/e2e/tests/ng-route-promise.spec.js
@@ -0,0 +1,36 @@
+'use strict';
+
+describe('ngRoute promises', function() {
+ beforeEach(function() {
+ loadFixture('ng-route-promise');
+ });
+
+ it('should wait for promises in resolve blocks', function() {
+ expect(element.all(by.tagName('li')).count()).toBe(5);
+ });
+
+ it('should time out if the promise takes long enough', function() {
+ // Don't try this at home kids, I'm a protractor dev
+ browser.manage().timeouts().setScriptTimeout(1500);
+
+ browser.waitForAngular().
+ then(onUnexpectedSuccess, onExpectedFailure).
+ then(restoreTimeoutLimit);
+
+ // Helpers
+ function onUnexpectedSuccess() {
+ fail('waitForAngular() should have timed out, but didn\'t');
+ }
+
+ function onExpectedFailure(error) {
+ expect(error.message).toContain(
+ 'Timed out waiting for asynchronous Angular tasks to finish after');
+ }
+
+ function restoreTimeoutLimit() {
+ return browser.getProcessedConfig().then(function(config) {
+ browser.manage().timeouts().setScriptTimeout(config.allScriptsTimeout);
+ });
+ }
+ });
+});
diff --git a/test/ngRoute/routeSpec.js b/test/ngRoute/routeSpec.js
index 9f0544bd4b2a..cbb517633f04 100644
--- a/test/ngRoute/routeSpec.js
+++ b/test/ngRoute/routeSpec.js
@@ -2082,4 +2082,58 @@ describe('$route', function() {
expect(function() { $route.updateParams(); }).toThrowMinErr('ngRoute', 'norout');
}));
});
+
+ describe('testability', function() {
+ it('should wait for route promises before calling callbacks', function() {
+ var deferred;
+
+ module(function($provide, $routeProvider) {
+ $routeProvider.when('/path', { template: '', resolve: {
+ a: function($q) {
+ deferred = $q.defer();
+ return deferred.promise;
+ }
+ } });
+ });
+
+ inject(function($location, $route, $rootScope, $httpBackend, $$testability) {
+ $location.path('/path');
+ $rootScope.$digest();
+
+ var callback = jasmine.createSpy('callback');
+ $$testability.whenStable(callback);
+ expect(callback).not.toHaveBeenCalled();
+
+ deferred.resolve();
+ $rootScope.$digest();
+ expect(callback).toHaveBeenCalled();
+ });
+ });
+
+ it('should call callback after route promises are rejected', function() {
+ var deferred;
+
+ module(function($provide, $routeProvider) {
+ $routeProvider.when('/path', { template: '', resolve: {
+ a: function($q) {
+ deferred = $q.defer();
+ return deferred.promise;
+ }
+ } });
+ });
+
+ inject(function($location, $route, $rootScope, $httpBackend, $$testability) {
+ $location.path('/path');
+ $rootScope.$digest();
+
+ var callback = jasmine.createSpy('callback');
+ $$testability.whenStable(callback);
+ expect(callback).not.toHaveBeenCalled();
+
+ deferred.reject();
+ $rootScope.$digest();
+ expect(callback).toHaveBeenCalled();
+ });
+ });
+ });
});