diff --git a/src/ngRoute/route.js b/src/ngRoute/route.js index cf5256c3dd51..7a48ab954666 100644 --- a/src/ngRoute/route.js +++ b/src/ngRoute/route.js @@ -70,12 +70,12 @@ function $RouteProvider() { * * Object properties: * - * - `controller` – `{(string|function()=}` – Controller fn that should be associated with + * - `controller` – `{(string|Function)=}` – Controller fn that should be associated with * newly created scope or the name of a {@link angular.Module#controller registered * controller} if passed as a string. * - `controllerAs` – `{string=}` – An identifier name for a reference to the controller. * If present, the controller will be published to scope under the `controllerAs` name. - * - `template` – `{string=|function()=}` – html template as a string or a function that + * - `template` – `{(string|Function)=}` – html template as a string or a function that * returns an html template as a string which should be used by {@link * ngRoute.directive:ngView ngView} or {@link ng.directive:ngInclude ngInclude} directives. * This property takes precedence over `templateUrl`. @@ -85,7 +85,7 @@ function $RouteProvider() { * - `{Array.}` - route parameters extracted from the current * `$location.path()` by applying the current route * - * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html + * - `templateUrl` – `{(string|Function)=}` – path or function that returns a path to an html * template that should be used by {@link ngRoute.directive:ngView ngView}. * * If `templateUrl` is a function, it will be called with the following parameters: @@ -93,7 +93,7 @@ function $RouteProvider() { * - `{Array.}` - route parameters extracted from the current * `$location.path()` by applying the current route * - * - `resolve` - `{Object.=}` - An optional map of dependencies which should + * - `resolve` - `{Object.=}` - An optional map of dependencies which should * be injected into the controller. If any of these dependencies are promises, the router * will wait for them all to be resolved or one to be rejected before the controller is * instantiated. @@ -113,7 +113,7 @@ function $RouteProvider() { * The map object is: * * - `key` – `{string}`: a name of a dependency to be injected into the controller. - * - `factory` - `{string|function}`: If `string` then it is an alias for a service. + * - `factory` - `{string|Function}`: If `string` then it is an alias for a service. * Otherwise if function, then it is {@link auto.$injector#invoke injected} * and the return value is treated as the dependency. If the result is a promise, it is * resolved before its value is injected into the controller. Be aware that @@ -123,7 +123,7 @@ function $RouteProvider() { * - `resolveAs` - `{string=}` - The name under which the `resolve` map will be available on * the scope of the route. If omitted, defaults to `$resolve`. * - * - `redirectTo` – `{(string|function())=}` – value to update + * - `redirectTo` – `{(string|Function)=}` – value to update * {@link ng.$location $location} path with and trigger route redirection. * * If `redirectTo` is a function, it will be called with the following parameters: @@ -134,7 +134,9 @@ function $RouteProvider() { * - `{Object}` - current `$location.search()` * * The custom `redirectTo` function is expected to return a string which will be used - * to update `$location.path()` and `$location.search()`. + * to update `$location.url()`. If the function throws an error, no further processing will + * take place and the {@link ngRoute.$route#$routeChangeError $routeChangeError} event will + * be fired. * * Routes that specify `redirectTo` will not have their controllers, template functions * or resolves called, the `$location` will be changed to the redirect url and route @@ -142,6 +144,22 @@ function $RouteProvider() { * returns `undefined`. In this case the route transition occurs as though there was no * redirection. * + * - `resolveRedirectTo` – `{Function=}` – a function that will (eventually) return the value + * to update {@link ng.$location $location} URL with and trigger route redirection. In + * contrast to `redirectTo`, dependencies can be injected into `resolveRedirectTo` and the + * return value can be either a string or a promise that will be resolved to a string. + * + * Similar to `redirectTo`, if the return value is `undefined` (or a promise that gets + * resolved to `undefined`), no redirection takes place and the route transition occurs as + * though there was no redirection. + * + * If the function throws an error or the returned promise gets rejected, no further + * processing will take place and the + * {@link ngRoute.$route#$routeChangeError $routeChangeError} event will be fired. + * + * `redirectTo` takes precedence over `resolveRedirectTo`, so specifying both on the same + * route definition, will cause the latter to be ignored. + * * - `[reloadOnSearch=true]` - `{boolean=}` - reload route when only `$location.search()` * or `$location.hash()` changes. * @@ -446,12 +464,14 @@ function $RouteProvider() { * @name $route#$routeChangeError * @eventType broadcast on root scope * @description - * Broadcasted if any of the resolve promises are rejected. + * Broadcasted if a redirection function fails or any redirection or resolve promises are + * rejected. * * @param {Object} angularEvent Synthetic event object * @param {Route} current Current route information. * @param {Route} previous Previous route information. - * @param {Route} rejection Rejection of the promise. Usually the error of the failed promise. + * @param {Route} rejection The thrown error or the rejection reason of the promise. Usually + * the rejection reason is the error that caused the promise to get rejected. */ /** @@ -592,37 +612,26 @@ function $RouteProvider() { } else if (nextRoute || lastRoute) { forceReload = false; $route.current = nextRoute; - if (nextRoute) { - if (nextRoute.redirectTo) { - var url = $location.url(); - var newUrl; - if (angular.isString(nextRoute.redirectTo)) { - $location.path(interpolate(nextRoute.redirectTo, nextRoute.params)) - .search(nextRoute.params) - .replace(); - newUrl = $location.url(); - } else { - newUrl = nextRoute.redirectTo(nextRoute.pathParams, $location.path(), $location.search()); - $location.url(newUrl).replace(); - } - if (angular.isDefined(newUrl) && url !== newUrl) { - return; //exit out and don't process current next value, wait for next location change from redirect - } - } - } - $q.when(nextRoute). - then(resolveLocals). - then(function(locals) { - // after route change - if (nextRoute === $route.current) { - if (nextRoute) { - nextRoute.locals = locals; - angular.copy(nextRoute.params, $routeParams); - } - $rootScope.$broadcast('$routeChangeSuccess', nextRoute, lastRoute); - } - }, function(error) { + var nextRoutePromise = $q.resolve(nextRoute); + + nextRoutePromise. + then(getRedirectionData). + then(handlePossibleRedirection). + then(function(keepProcessingRoute) { + return keepProcessingRoute && nextRoutePromise. + then(resolveLocals). + then(function(locals) { + // after route change + if (nextRoute === $route.current) { + if (nextRoute) { + nextRoute.locals = locals; + angular.copy(nextRoute.params, $routeParams); + } + $rootScope.$broadcast('$routeChangeSuccess', nextRoute, lastRoute); + } + }); + }).catch(function(error) { if (nextRoute === $route.current) { $rootScope.$broadcast('$routeChangeError', nextRoute, lastRoute, error); } @@ -630,6 +639,76 @@ function $RouteProvider() { } } + function getRedirectionData(route) { + var data = { + route: route, + hasRedirection: false + }; + + if (route) { + if (route.redirectTo) { + if (angular.isString(route.redirectTo)) { + data.path = interpolate(route.redirectTo, route.params); + data.search = route.params; + data.hasRedirection = true; + } else { + var oldPath = $location.path(); + var oldSearch = $location.search(); + var newUrl = route.redirectTo(route.pathParams, oldPath, oldSearch); + + if (angular.isDefined(newUrl)) { + data.url = newUrl; + data.hasRedirection = true; + } + } + } else if (route.resolveRedirectTo) { + return $q. + resolve($injector.invoke(route.resolveRedirectTo)). + then(function(newUrl) { + if (angular.isDefined(newUrl)) { + data.url = newUrl; + data.hasRedirection = true; + } + + return data; + }); + } + } + + return data; + } + + function handlePossibleRedirection(data) { + var keepProcessingRoute = true; + + if (data.route !== $route.current) { + keepProcessingRoute = false; + } else if (data.hasRedirection) { + var oldUrl = $location.url(); + var newUrl = data.url; + + if (newUrl) { + $location. + url(newUrl). + replace(); + } else { + newUrl = $location. + path(data.path). + search(data.search). + replace(). + url(); + } + + if (newUrl !== oldUrl) { + // Exit out and don't process current next value, + // wait for next location change from redirect + keepProcessingRoute = false; + } + } + + return keepProcessingRoute; + } + function resolveLocals(route) { if (route) { var locals = angular.extend({}, route.resolve); @@ -646,7 +725,6 @@ function $RouteProvider() { } } - function getTemplateFor(route) { var template, templateUrl; if (angular.isDefined(template = route.template)) { @@ -665,7 +743,6 @@ function $RouteProvider() { return template; } - /** * @returns {Object} the current active route, by matching it against the URL */ diff --git a/test/ngRoute/routeSpec.js b/test/ngRoute/routeSpec.js index aa1d283fda83..ae7636aac3ce 100644 --- a/test/ngRoute/routeSpec.js +++ b/test/ngRoute/routeSpec.js @@ -901,223 +901,632 @@ describe('$route', function() { describe('redirection', function() { - it('should support redirection via redirectTo property by updating $location', function() { - module(function($routeProvider) { - $routeProvider.when('/', {redirectTo: '/foo'}); - $routeProvider.when('/foo', {templateUrl: 'foo.html'}); - $routeProvider.when('/bar', {templateUrl: 'bar.html'}); - $routeProvider.when('/baz', {redirectTo: '/bar'}); - $routeProvider.otherwise({templateUrl: '404.html'}); + describe('via `redirectTo`', function() { + it('should support redirection via redirectTo property by updating $location', function() { + module(function($routeProvider) { + $routeProvider.when('/', {redirectTo: '/foo'}); + $routeProvider.when('/foo', {templateUrl: 'foo.html'}); + $routeProvider.when('/bar', {templateUrl: 'bar.html'}); + $routeProvider.when('/baz', {redirectTo: '/bar'}); + $routeProvider.otherwise({templateUrl: '404.html'}); + }); + + inject(function($route, $location, $rootScope) { + var onChangeSpy = jasmine.createSpy('onChange'); + + $rootScope.$on('$routeChangeStart', onChangeSpy); + expect($route.current).toBeUndefined(); + expect(onChangeSpy).not.toHaveBeenCalled(); + + $location.path('/'); + $rootScope.$digest(); + expect($location.path()).toBe('/foo'); + expect($route.current.templateUrl).toBe('foo.html'); + expect(onChangeSpy).toHaveBeenCalledTimes(2); + + onChangeSpy.calls.reset(); + $location.path('/baz'); + $rootScope.$digest(); + expect($location.path()).toBe('/bar'); + expect($route.current.templateUrl).toBe('bar.html'); + expect(onChangeSpy).toHaveBeenCalledTimes(2); + }); }); - inject(function($route, $location, $rootScope) { - var onChangeSpy = jasmine.createSpy('onChange'); - $rootScope.$on('$routeChangeStart', onChangeSpy); - expect($route.current).toBeUndefined(); - expect(onChangeSpy).not.toHaveBeenCalled(); + it('should interpolate route vars in the redirected path from original path', function() { + module(function($routeProvider) { + $routeProvider.when('/foo/:id/foo/:subid/:extraId', {redirectTo: '/bar/:id/:subid/23'}); + $routeProvider.when('/bar/:id/:subid/:subsubid', {templateUrl: 'bar.html'}); + $routeProvider.when('/baz/:id/:path*', {redirectTo: '/path/:path/:id'}); + $routeProvider.when('/path/:path*/:id', {templateUrl: 'foo.html'}); + }); - $location.path('/'); - $rootScope.$digest(); - expect($location.path()).toBe('/foo'); - expect($route.current.templateUrl).toBe('foo.html'); - expect(onChangeSpy).toHaveBeenCalledTimes(2); + inject(function($route, $location, $rootScope) { + $location.path('/foo/id1/foo/subid3/gah'); + $rootScope.$digest(); - onChangeSpy.calls.reset(); - $location.path('/baz'); - $rootScope.$digest(); - expect($location.path()).toBe('/bar'); - expect($route.current.templateUrl).toBe('bar.html'); - expect(onChangeSpy).toHaveBeenCalledTimes(2); + expect($location.path()).toEqual('/bar/id1/subid3/23'); + expect($location.search()).toEqual({extraId: 'gah'}); + expect($route.current.templateUrl).toEqual('bar.html'); + + $location.path('/baz/1/foovalue/barvalue'); + $rootScope.$digest(); + expect($location.path()).toEqual('/path/foovalue/barvalue/1'); + expect($route.current.templateUrl).toEqual('foo.html'); + }); }); - }); - it('should interpolate route vars in the redirected path from original path', function() { - module(function($routeProvider) { - $routeProvider.when('/foo/:id/foo/:subid/:extraId', {redirectTo: '/bar/:id/:subid/23'}); - $routeProvider.when('/bar/:id/:subid/:subsubid', {templateUrl: 'bar.html'}); - $routeProvider.when('/baz/:id/:path*', {redirectTo: '/path/:path/:id'}); - $routeProvider.when('/path/:path*/:id', {templateUrl: 'foo.html'}); + it('should interpolate route vars in the redirected path from original search', function() { + module(function($routeProvider) { + $routeProvider.when('/bar/:id/:subid/:subsubid', {templateUrl: 'bar.html'}); + $routeProvider.when('/foo/:id/:extra', {redirectTo: '/bar/:id/:subid/99'}); + }); + + inject(function($route, $location, $rootScope) { + $location.path('/foo/id3/eId').search('subid=sid1&appended=true'); + $rootScope.$digest(); + + expect($location.path()).toEqual('/bar/id3/sid1/99'); + expect($location.search()).toEqual({appended: 'true', extra: 'eId'}); + expect($route.current.templateUrl).toEqual('bar.html'); + }); }); - inject(function($route, $location, $rootScope) { - $location.path('/foo/id1/foo/subid3/gah'); - $rootScope.$digest(); - expect($location.path()).toEqual('/bar/id1/subid3/23'); - expect($location.search()).toEqual({extraId: 'gah'}); - expect($route.current.templateUrl).toEqual('bar.html'); + it('should properly process route params which are both eager and optional', function() { + module(function($routeProvider) { + $routeProvider.when('/foo/:param1*?/:param2', {templateUrl: 'foo.html'}); + }); - $location.path('/baz/1/foovalue/barvalue'); - $rootScope.$digest(); - expect($location.path()).toEqual('/path/foovalue/barvalue/1'); - expect($route.current.templateUrl).toEqual('foo.html'); + inject(function($location, $rootScope, $route) { + $location.path('/foo/bar1/bar2/bar3/baz'); + $rootScope.$digest(); + + expect($location.path()).toEqual('/foo/bar1/bar2/bar3/baz'); + expect($route.current.params.param1).toEqual('bar1/bar2/bar3'); + expect($route.current.params.param2).toEqual('baz'); + expect($route.current.templateUrl).toEqual('foo.html'); + + $location.path('/foo/baz'); + $rootScope.$digest(); + + expect($location.path()).toEqual('/foo/baz'); + expect($route.current.params.param1).toEqual(undefined); + expect($route.current.params.param2).toEqual('baz'); + expect($route.current.templateUrl).toEqual('foo.html'); + + }); }); - }); - it('should interpolate route vars in the redirected path from original search', function() { - module(function($routeProvider) { - $routeProvider.when('/bar/:id/:subid/:subsubid', {templateUrl: 'bar.html'}); - $routeProvider.when('/foo/:id/:extra', {redirectTo: '/bar/:id/:subid/99'}); + it('should properly interpolate optional and eager route vars ' + + 'when redirecting from path with trailing slash', function() { + module(function($routeProvider) { + $routeProvider.when('/foo/:id?/:subid?', {templateUrl: 'foo.html'}); + $routeProvider.when('/bar/:id*/:subid', {templateUrl: 'bar.html'}); + }); + + inject(function($location, $rootScope, $route) { + $location.path('/foo/id1/subid2/'); + $rootScope.$digest(); + + expect($location.path()).toEqual('/foo/id1/subid2'); + expect($route.current.templateUrl).toEqual('foo.html'); + + $location.path('/bar/id1/extra/subid2/'); + $rootScope.$digest(); + + expect($location.path()).toEqual('/bar/id1/extra/subid2'); + expect($route.current.templateUrl).toEqual('bar.html'); + }); }); - inject(function($route, $location, $rootScope) { - $location.path('/foo/id3/eId').search('subid=sid1&appended=true'); - $rootScope.$digest(); - expect($location.path()).toEqual('/bar/id3/sid1/99'); - expect($location.search()).toEqual({appended: 'true', extra: 'eId'}); - expect($route.current.templateUrl).toEqual('bar.html'); + it('should allow custom redirectTo function to be used', function() { + function customRedirectFn(routePathParams, path, search) { + expect(routePathParams).toEqual({id: 'id3'}); + expect(path).toEqual('/foo/id3'); + expect(search).toEqual({subid: 'sid1', appended: 'true'}); + return '/custom'; + } + + module(function($routeProvider) { + $routeProvider.when('/foo/:id', {redirectTo: customRedirectFn}); + }); + + inject(function($route, $location, $rootScope) { + $location.path('/foo/id3').search('subid=sid1&appended=true'); + $rootScope.$digest(); + + expect($location.path()).toEqual('/custom'); + }); }); - }); - it('should properly process route params which are both eager and optional', function() { - module(function($routeProvider) { - $routeProvider.when('/foo/:param1*?/:param2', {templateUrl: 'foo.html'}); + it('should broadcast `$routeChangeError` when redirectTo throws', function() { + var error = new Error('Test'); + + module(function($exceptionHandlerProvider, $routeProvider) { + $exceptionHandlerProvider.mode('log'); + $routeProvider.when('/foo', {redirectTo: function() { throw error; }}); + }); + + inject(function($exceptionHandler, $location, $rootScope, $route) { + spyOn($rootScope, '$broadcast').and.callThrough(); + + $location.path('/foo'); + $rootScope.$digest(); + + var lastCallArgs = $rootScope.$broadcast.calls.mostRecent().args; + expect(lastCallArgs[0]).toBe('$routeChangeError'); + expect(lastCallArgs[3]).toBe(error); + expect($exceptionHandler.errors[0]).toBe(error); + }); }); - inject(function($location, $rootScope, $route) { - $location.path('/foo/bar1/bar2/bar3/baz'); - $rootScope.$digest(); - expect($location.path()).toEqual('/foo/bar1/bar2/bar3/baz'); - expect($route.current.params.param1).toEqual('bar1/bar2/bar3'); - expect($route.current.params.param2).toEqual('baz'); - expect($route.current.templateUrl).toEqual('foo.html'); + it('should replace the url when redirecting', function() { + module(function($routeProvider) { + $routeProvider.when('/bar/:id', {templateUrl: 'bar.html'}); + $routeProvider.when('/foo/:id/:extra', {redirectTo: '/bar/:id'}); + }); + inject(function($browser, $route, $location, $rootScope) { + var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').and.callThrough(); - $location.path('/foo/baz'); - $rootScope.$digest(); + $location.path('/foo/id3/eId'); + $rootScope.$digest(); + + expect($location.path()).toEqual('/bar/id3'); + expect($browserUrl.calls.mostRecent().args) + .toEqual(['http://server/#!/bar/id3?extra=eId', true, null]); + }); + }); - expect($location.path()).toEqual('/foo/baz'); - expect($route.current.params.param1).toEqual(undefined); - expect($route.current.params.param2).toEqual('baz'); - expect($route.current.templateUrl).toEqual('foo.html'); + it('should not process route bits', function() { + var firstController = jasmine.createSpy('first controller spy'); + var firstTemplate = jasmine.createSpy('first template spy').and.returnValue('redirected view'); + var firstResolve = jasmine.createSpy('first resolve spy'); + var secondController = jasmine.createSpy('second controller spy'); + var secondTemplate = jasmine.createSpy('second template spy').and.returnValue('redirected view'); + var secondResolve = jasmine.createSpy('second resolve spy'); + module(function($routeProvider) { + $routeProvider.when('/redirect', { + template: firstTemplate, + redirectTo: '/redirected', + resolve: { value: firstResolve }, + controller: firstController + }); + $routeProvider.when('/redirected', { + template: secondTemplate, + resolve: { value: secondResolve }, + controller: secondController + }); + }); + inject(function($route, $location, $rootScope, $compile) { + var element = $compile('
')($rootScope); + $location.path('/redirect'); + $rootScope.$digest(); + + expect(firstController).not.toHaveBeenCalled(); + expect(firstTemplate).not.toHaveBeenCalled(); + expect(firstResolve).not.toHaveBeenCalled(); + + expect(secondController).toHaveBeenCalled(); + expect(secondTemplate).toHaveBeenCalled(); + expect(secondResolve).toHaveBeenCalled(); + + dealoc(element); + }); + }); + + + it('should not redirect transition if `redirectTo` returns `undefined`', function() { + var controller = jasmine.createSpy('first controller spy'); + var templateFn = jasmine.createSpy('first template spy').and.returnValue('redirected view'); + module(function($routeProvider) { + $routeProvider.when('/redirect/to/undefined', { + template: templateFn, + redirectTo: function() {}, + controller: controller + }); + }); + inject(function($route, $location, $rootScope, $compile) { + var element = $compile('
')($rootScope); + $location.path('/redirect/to/undefined'); + $rootScope.$digest(); + expect(controller).toHaveBeenCalled(); + expect(templateFn).toHaveBeenCalled(); + expect($location.path()).toEqual('/redirect/to/undefined'); + dealoc(element); + }); }); }); + describe('via `resolveRedirectTo`', function() { + var $compile; + var $location; + var $rootScope; + var $route; - it('should properly interpolate optional and eager route vars ' + - 'when redirecting from path with trailing slash', function() { - module(function($routeProvider) { - $routeProvider.when('/foo/:id?/:subid?', {templateUrl: 'foo.html'}); - $routeProvider.when('/bar/:id*/:subid', {templateUrl: 'bar.html'}); + beforeEach(module(function() { + return function(_$compile_, _$location_, _$rootScope_, _$route_) { + $compile = _$compile_; + $location = _$location_; + $rootScope = _$rootScope_; + $route = _$route_; + }; + })); + + + it('should be ignored if `redirectTo` is also present', function() { + var newUrl; + var getNewUrl = function() { return newUrl; }; + + var resolveRedirectToSpy = jasmine.createSpy('resolveRedirectTo').and.returnValue('/bar'); + var redirectToSpy = jasmine.createSpy('redirectTo').and.callFake(getNewUrl); + var templateSpy = jasmine.createSpy('template').and.returnValue('Foo'); + + module(function($routeProvider) { + $routeProvider. + when('/foo', { + resolveRedirectTo: resolveRedirectToSpy, + redirectTo: redirectToSpy, + template: templateSpy + }). + when('/bar', {template: 'Bar'}). + when('/baz', {template: 'Baz'}); + }); + + inject(function() { + newUrl = '/baz'; + $location.path('/foo'); + $rootScope.$digest(); + + expect($location.path()).toBe('/baz'); + expect($route.current.template).toBe('Baz'); + expect(resolveRedirectToSpy).not.toHaveBeenCalled(); + expect(redirectToSpy).toHaveBeenCalled(); + expect(templateSpy).not.toHaveBeenCalled(); + + redirectToSpy.calls.reset(); + + newUrl = undefined; + $location.path('/foo'); + $rootScope.$digest(); + + expect($location.path()).toBe('/foo'); + expect($route.current.template).toBe(templateSpy); + expect(resolveRedirectToSpy).not.toHaveBeenCalled(); + expect(redirectToSpy).toHaveBeenCalled(); + expect(templateSpy).toHaveBeenCalled(); + }); }); - inject(function($location, $rootScope, $route) { - $location.path('/foo/id1/subid2/'); - $rootScope.$digest(); - expect($location.path()).toEqual('/foo/id1/subid2'); - expect($route.current.templateUrl).toEqual('foo.html'); + it('should redirect to the returned url', function() { + module(function($routeProvider) { + $routeProvider. + when('/foo', {resolveRedirectTo: function() { return '/bar?baz=qux'; }}). + when('/bar', {template: 'Bar'}); + }); - $location.path('/bar/id1/extra/subid2/'); - $rootScope.$digest(); + inject(function() { + $location.path('/foo'); + $rootScope.$digest(); - expect($location.path()).toEqual('/bar/id1/extra/subid2'); - expect($route.current.templateUrl).toEqual('bar.html'); + expect($location.path()).toBe('/bar'); + expect($location.search()).toEqual({baz: 'qux'}); + expect($route.current.template).toBe('Bar'); + }); }); - }); - it('should allow custom redirectTo function to be used', function() { - function customRedirectFn(routePathParams, path, search) { - expect(routePathParams).toEqual({id: 'id3'}); - expect(path).toEqual('/foo/id3'); - expect(search).toEqual({ subid: 'sid1', appended: 'true' }); - return '/custom'; - } + it('should support returning a promise', function() { + module(function($routeProvider) { + $routeProvider. + when('/foo', {resolveRedirectTo: function($q) { return $q.resolve('/bar'); }}). + when('/bar', {template: 'Bar'}); + }); - module(function($routeProvider) { - $routeProvider.when('/bar/:id/:subid/:subsubid', {templateUrl: 'bar.html'}); - $routeProvider.when('/foo/:id', {redirectTo: customRedirectFn}); + inject(function() { + $location.path('/foo'); + $rootScope.$digest(); + + expect($location.path()).toBe('/bar'); + expect($route.current.template).toBe('Bar'); + }); }); - inject(function($route, $location, $rootScope) { - $location.path('/foo/id3').search('subid=sid1&appended=true'); - $rootScope.$digest(); - expect($location.path()).toEqual('/custom'); + it('should support dependency injection', function() { + module(function($provide, $routeProvider) { + $provide.value('nextRoute', '/bar'); + + $routeProvider. + when('/foo', { + resolveRedirectTo: function(nextRoute) { + return nextRoute; + } + }); + }); + + inject(function() { + $location.path('/foo'); + $rootScope.$digest(); + + expect($location.path()).toBe('/bar'); + }); }); - }); - it('should replace the url when redirecting', function() { - module(function($routeProvider) { - $routeProvider.when('/bar/:id', {templateUrl: 'bar.html'}); - $routeProvider.when('/foo/:id/:extra', {redirectTo: '/bar/:id'}); + it('should have access to the current routeParams via `$route.current.params`', function() { + module(function($routeProvider) { + $routeProvider. + when('/foo/:bar/baz/:qux', { + resolveRedirectTo: function($route) { + expect($route.current.params).toEqual(jasmine.objectContaining({ + bar: '1', + qux: '2' + })); + + return '/passed'; + } + }); + }); + + inject(function() { + $location.path('/foo/1/baz/2').search({bar: 'qux'}); + $rootScope.$digest(); + + expect($location.path()).toBe('/passed'); + }); }); - inject(function($browser, $route, $location, $rootScope) { - var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').and.callThrough(); - $location.path('/foo/id3/eId'); - $rootScope.$digest(); - expect($location.path()).toEqual('/bar/id3'); - expect($browserUrl.calls.mostRecent().args) - .toEqual(['http://server/#!/bar/id3?extra=eId', true, null]); + it('should not process route bits until the promise is resolved', function() { + var spies = createSpies(); + var called = false; + var deferred; + + module(function($routeProvider) { + setupRoutes($routeProvider, spies, function($q) { + called = true; + deferred = $q.defer(); + return deferred.promise; + }); + }); + + inject(function() { + var element = $compile('
')($rootScope); + + $location.path('/foo'); + $rootScope.$digest(); + + expect($location.path()).toBe('/foo'); + expect(called).toBe(true); + expect(spies.fooResolveSpy).not.toHaveBeenCalled(); + expect(spies.fooTemplateSpy).not.toHaveBeenCalled(); + expect(spies.fooControllerSpy).not.toHaveBeenCalled(); + expect(spies.barResolveSpy).not.toHaveBeenCalled(); + expect(spies.barTemplateSpy).not.toHaveBeenCalled(); + expect(spies.barControllerSpy).not.toHaveBeenCalled(); + + deferred.resolve('/bar'); + $rootScope.$digest(); + expect($location.path()).toBe('/bar'); + expect(spies.fooResolveSpy).not.toHaveBeenCalled(); + expect(spies.fooTemplateSpy).not.toHaveBeenCalled(); + expect(spies.fooControllerSpy).not.toHaveBeenCalled(); + expect(spies.barResolveSpy).toHaveBeenCalled(); + expect(spies.barTemplateSpy).toHaveBeenCalled(); + expect(spies.barControllerSpy).toHaveBeenCalled(); + + dealoc(element); + }); }); - }); - it('should not process route bits', function() { - var firstController = jasmine.createSpy('first controller spy'); - var firstTemplate = jasmine.createSpy('first template spy').and.returnValue('redirected view'); - var firstResolve = jasmine.createSpy('first resolve spy'); - var secondController = jasmine.createSpy('second controller spy'); - var secondTemplate = jasmine.createSpy('second template spy').and.returnValue('redirected view'); - var secondResolve = jasmine.createSpy('second resolve spy'); - module(function($routeProvider) { - $routeProvider.when('/redirect', { - template: firstTemplate, - redirectTo: '/redirected', - resolve: { value: firstResolve }, - controller: firstController + + it('should not redirect if `undefined` is returned', function() { + var spies = createSpies(); + var called = false; + + module(function($routeProvider) { + setupRoutes($routeProvider, spies, function() { + called = true; + return undefined; + }); }); - $routeProvider.when('/redirected', { - template: secondTemplate, - resolve: { value: secondResolve }, - controller: secondController + + inject(function() { + var element = $compile('
')($rootScope); + + $location.path('/foo'); + $rootScope.$digest(); + + expect($location.path()).toBe('/foo'); + expect(called).toBe(true); + expect(spies.fooResolveSpy).toHaveBeenCalled(); + expect(spies.fooTemplateSpy).toHaveBeenCalled(); + expect(spies.fooControllerSpy).toHaveBeenCalled(); + expect(spies.barResolveSpy).not.toHaveBeenCalled(); + expect(spies.barTemplateSpy).not.toHaveBeenCalled(); + expect(spies.barControllerSpy).not.toHaveBeenCalled(); + + dealoc(element); }); }); - inject(function($route, $location, $rootScope, $compile) { - var element = $compile('
')($rootScope); - $location.path('/redirect'); - $rootScope.$digest(); - expect(firstController).not.toHaveBeenCalled(); - expect(firstTemplate).not.toHaveBeenCalled(); - expect(firstResolve).not.toHaveBeenCalled(); - expect(secondController).toHaveBeenCalled(); - expect(secondTemplate).toHaveBeenCalled(); - expect(secondResolve).toHaveBeenCalled(); + it('should not redirect if the returned promise resolves to `undefined`', function() { + var spies = createSpies(); + var called = false; + + module(function($routeProvider) { + setupRoutes($routeProvider, spies, function($q) { + called = true; + return $q.resolve(undefined); + }); + }); + + inject(function() { + var element = $compile('
')($rootScope); + + $location.path('/foo'); + $rootScope.$digest(); - dealoc(element); + expect($location.path()).toBe('/foo'); + expect(called).toBe(true); + expect(spies.fooResolveSpy).toHaveBeenCalled(); + expect(spies.fooTemplateSpy).toHaveBeenCalled(); + expect(spies.fooControllerSpy).toHaveBeenCalled(); + expect(spies.barResolveSpy).not.toHaveBeenCalled(); + expect(spies.barTemplateSpy).not.toHaveBeenCalled(); + expect(spies.barControllerSpy).not.toHaveBeenCalled(); + + dealoc(element); + }); }); - }); - it('should not redirect transition if `redirectTo` returns `undefined`', function() { - var controller = jasmine.createSpy('first controller spy'); - var templateFn = jasmine.createSpy('first template spy').and.returnValue('redirected view'); - module(function($routeProvider) { - $routeProvider.when('/redirect/to/undefined', { - template: templateFn, - redirectTo: function() {}, - controller: controller + + it('should not redirect if the returned promise gets rejected', function() { + var spies = createSpies(); + var called = false; + + module(function($routeProvider) { + setupRoutes($routeProvider, spies, function($q) { + called = true; + return $q.reject(''); + }); + }); + + inject(function() { + spyOn($rootScope, '$broadcast').and.callThrough(); + + var element = $compile('
')($rootScope); + + $location.path('/foo'); + $rootScope.$digest(); + + expect($location.path()).toBe('/foo'); + expect(called).toBe(true); + expect(spies.fooResolveSpy).not.toHaveBeenCalled(); + expect(spies.fooTemplateSpy).not.toHaveBeenCalled(); + expect(spies.fooControllerSpy).not.toHaveBeenCalled(); + expect(spies.barResolveSpy).not.toHaveBeenCalled(); + expect(spies.barTemplateSpy).not.toHaveBeenCalled(); + expect(spies.barControllerSpy).not.toHaveBeenCalled(); + + var lastCallArgs = $rootScope.$broadcast.calls.mostRecent().args; + expect(lastCallArgs[0]).toBe('$routeChangeError'); + + dealoc(element); }); }); - inject(function($route, $location, $rootScope, $compile) { - var element = $compile('
')($rootScope); - $location.path('/redirect/to/undefined'); - $rootScope.$digest(); - expect(controller).toHaveBeenCalled(); - expect(templateFn).toHaveBeenCalled(); - expect($location.path()).toEqual('/redirect/to/undefined'); - dealoc(element); + + + it('should ignore previous redirection if newer transition happened', function() { + var spies = createSpies(); + var called = false; + var deferred; + + module(function($routeProvider) { + setupRoutes($routeProvider, spies, function($q) { + called = true; + deferred = $q.defer(); + return deferred.promise; + }); + }); + + inject(function() { + spyOn($location, 'url').and.callThrough(); + + var element = $compile('
')($rootScope); + + $location.path('/foo'); + $rootScope.$digest(); + + expect($location.path()).toBe('/foo'); + expect(called).toBe(true); + expect(spies.fooResolveSpy).not.toHaveBeenCalled(); + expect(spies.fooTemplateSpy).not.toHaveBeenCalled(); + expect(spies.fooControllerSpy).not.toHaveBeenCalled(); + expect(spies.barResolveSpy).not.toHaveBeenCalled(); + expect(spies.barTemplateSpy).not.toHaveBeenCalled(); + expect(spies.barControllerSpy).not.toHaveBeenCalled(); + expect(spies.bazResolveSpy).not.toHaveBeenCalled(); + expect(spies.bazTemplateSpy).not.toHaveBeenCalled(); + expect(spies.bazControllerSpy).not.toHaveBeenCalled(); + + $location.path('/baz'); + $rootScope.$digest(); + + expect($location.path()).toBe('/baz'); + expect(spies.fooResolveSpy).not.toHaveBeenCalled(); + expect(spies.fooTemplateSpy).not.toHaveBeenCalled(); + expect(spies.fooControllerSpy).not.toHaveBeenCalled(); + expect(spies.barResolveSpy).not.toHaveBeenCalled(); + expect(spies.barTemplateSpy).not.toHaveBeenCalled(); + expect(spies.barControllerSpy).not.toHaveBeenCalled(); + expect(spies.bazResolveSpy).toHaveBeenCalledOnce(); + expect(spies.bazTemplateSpy).toHaveBeenCalledOnce(); + expect(spies.bazControllerSpy).toHaveBeenCalledOnce(); + + deferred.resolve(); + $rootScope.$digest(); + + expect($location.path()).toBe('/baz'); + expect(spies.fooResolveSpy).not.toHaveBeenCalled(); + expect(spies.fooTemplateSpy).not.toHaveBeenCalled(); + expect(spies.fooControllerSpy).not.toHaveBeenCalled(); + expect(spies.barResolveSpy).not.toHaveBeenCalled(); + expect(spies.barTemplateSpy).not.toHaveBeenCalled(); + expect(spies.barControllerSpy).not.toHaveBeenCalled(); + expect(spies.bazResolveSpy).toHaveBeenCalledOnce(); + expect(spies.bazTemplateSpy).toHaveBeenCalledOnce(); + expect(spies.bazControllerSpy).toHaveBeenCalledOnce(); + + dealoc(element); + }); }); + + + // Helpers + function createSpies() { + return { + fooResolveSpy: jasmine.createSpy('fooResolve'), + fooTemplateSpy: jasmine.createSpy('fooTemplate').and.returnValue('Foo'), + fooControllerSpy: jasmine.createSpy('fooController'), + barResolveSpy: jasmine.createSpy('barResolve'), + barTemplateSpy: jasmine.createSpy('barTemplate').and.returnValue('Bar'), + barControllerSpy: jasmine.createSpy('barController'), + bazResolveSpy: jasmine.createSpy('bazResolve'), + bazTemplateSpy: jasmine.createSpy('bazTemplate').and.returnValue('Baz'), + bazControllerSpy: jasmine.createSpy('bazController') + }; + } + + function setupRoutes(routeProvider, spies, resolveRedirectToFn) { + routeProvider. + when('/foo', { + resolveRedirectTo: resolveRedirectToFn, + resolve: {_: spies.fooResolveSpy}, + template: spies.fooTemplateSpy, + controller: spies.fooControllerSpy + }). + when('/bar', { + resolve: {_: spies.barResolveSpy}, + template: spies.barTemplateSpy, + controller: spies.barControllerSpy + }). + when('/baz', { + resolve: {_: spies.bazResolveSpy}, + template: spies.bazTemplateSpy, + controller: spies.bazControllerSpy + }); + } }); });