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(); + }); + }); + }); });