')($rootScope);
+ $location.url('https://github.com/view');
+ $rootScope.$apply();
+ expect(contentOnLink).toBe('someContent');
+ });
+ });
+
+ it('should add the content to the element before compiling it', function() {
+ var root;
+ module(function($compileProvider, $routeProvider) {
+ $routeProvider.when('/view', {template: '
'});
+ $compileProvider.directive('test', function() {
+ return {
+ link: function(scope, element) {
+ root = element.parent().parent();
+ }
+ };
+ });
+ });
+ inject(function($compile, $rootScope, $location) {
+ element = $compile('
')($rootScope);
+ $location.url('https://github.com/view');
+ $rootScope.$apply();
+ expect(root[0]).toBe(element[0]);
+ });
+ });
+ });
+
+ describe('animations', function() {
+ var body, element, $rootElement;
+
+ beforeEach(module('ngRoute'));
+
+ function html(content) {
+ $rootElement.html(content);
+ body.append($rootElement);
+ element = $rootElement.children().eq(0);
+ return element;
+ }
+
+ beforeEach(module(function() {
+ // we need to run animation on attached elements;
+ return function(_$rootElement_) {
+ $rootElement = _$rootElement_;
+ body = angular.element(window.document.body);
+ };
+ }));
+
+ afterEach(function() {
+ dealoc(body);
+ dealoc(element);
+ });
+
+
+ beforeEach(module(function($provide, $routeProvider) {
+ $routeProvider.when('/foo', {controller: angular.noop, templateUrl: '/foo.html'});
+ $routeProvider.when('/bar', {controller: angular.noop, templateUrl: '/bar.html'});
+ return function($templateCache) {
+ $templateCache.put('/foo.html', [200, '
data
', {}]);
+ $templateCache.put('/bar.html', [200, '
data2
', {}]);
+ };
+ }));
+
+ describe('hooks', function() {
+ beforeEach(module('ngAnimate'));
+ beforeEach(module('ngAnimateMock'));
+
+ it('should fire off the enter animation',
+ inject(function($compile, $rootScope, $location, $timeout, $animate) {
+ element = $compile(html('
'))($rootScope);
+
+ $location.path('/foo');
+ $rootScope.$digest();
+
+ var animation = $animate.queue.pop();
+ expect(animation.event).toBe('enter');
+ }));
+
+ it('should fire off the leave animation',
+ inject(function($compile, $rootScope, $location, $templateCache, $timeout, $animate) {
+
+ var item;
+ $templateCache.put('/foo.html', [200, '
foo
', {}]);
+ element = $compile(html('
'))($rootScope);
+
+ $location.path('/foo');
+ $rootScope.$digest();
+
+
+ $location.path('/');
+ $rootScope.$digest();
+
+ var animation = $animate.queue.pop();
+ expect(animation.event).toBe('leave');
+ }));
+
+ it('should animate two separate ngView elements',
+ inject(function($compile, $rootScope, $templateCache, $location, $animate) {
+ var item;
+ $rootScope.tpl = 'one';
+ element = $compile(html('
'))($rootScope);
+ $rootScope.$digest();
+
+ $location.path('/foo');
+ $rootScope.$digest();
+
+ //we don't care about the enter animation for the first element
+ $animate.queue.pop();
+
+ $location.path('/bar');
+ $rootScope.$digest();
+
+ var animationB = $animate.queue.pop();
+ expect(animationB.event).toBe('leave');
+ var itemB = animationB.args[0];
+
+ var animationA = $animate.queue.pop();
+ expect(animationA.event).toBe('enter');
+ var itemA = animationA.args[0];
+
+ expect(itemA).not.toEqual(itemB);
+ })
+ );
+
+ it('should render ngClass on ngView',
+ inject(function($compile, $rootScope, $templateCache, $animate, $location) {
+
+ var item;
+ $rootScope.tpl = 'one';
+ $rootScope.klass = 'classy';
+ element = $compile(html('
'))($rootScope);
+ $rootScope.$digest();
+
+ $location.path('/foo');
+ $rootScope.$digest();
+ $animate.flush();
+
+ //we don't care about the enter animation
+ $animate.queue.shift();
+
+ var animation = $animate.queue.shift();
+ expect(animation.event).toBe('addClass');
+
+ item = animation.element;
+ expect(item.hasClass('classy')).toBe(true);
+
+ $rootScope.klass = 'boring';
+ $rootScope.$digest();
+
+ expect($animate.queue.shift().event).toBe('addClass');
+ expect($animate.queue.shift().event).toBe('removeClass');
+
+ $animate.flush();
+
+ expect(item.hasClass('classy')).toBe(false);
+ expect(item.hasClass('boring')).toBe(true);
+
+ $location.path('/bar');
+ $rootScope.$digest();
+
+ //we don't care about the enter animation
+ $animate.queue.shift();
+
+ animation = $animate.queue.shift();
+ item = animation.element;
+ expect(animation.event).toBe('leave');
+
+ expect($animate.queue.shift().event).toBe('addClass');
+
+ expect(item.hasClass('boring')).toBe(true);
+ })
+ );
+
+ it('should not double compile when the route changes', function() {
+
+ var window;
+ module(function($routeProvider, $animateProvider, $provide) {
+ $routeProvider.when('/foo', {template: '
{{i}}
'});
+ $routeProvider.when('/bar', {template: '
{{i}}
'});
+ $animateProvider.register('.my-animation', function() {
+ return {
+ leave: function(element, done) {
+ done();
+ }
+ };
+ });
+ });
+
+ inject(function($rootScope, $compile, $location, $route, $timeout, $rootElement, $sniffer, $animate) {
+ element = $compile(html('
'))($rootScope);
+ $animate.enabled(true);
+
+ $location.path('/foo');
+ $rootScope.$digest();
+
+ expect($animate.queue.shift().event).toBe('enter'); //ngView
+ expect($animate.queue.shift().event).toBe('enter'); //repeat 1
+ expect($animate.queue.shift().event).toBe('enter'); //repeat 2
+
+ expect(element.text()).toEqual('12');
+
+ $location.path('/bar');
+ $rootScope.$digest();
+
+ expect($animate.queue.shift().event).toBe('enter'); //ngView new
+ expect($animate.queue.shift().event).toBe('leave'); //ngView old
+
+ $rootScope.$digest();
+
+ expect($animate.queue.shift().event).toBe('enter'); //ngRepeat 3
+ expect($animate.queue.shift().event).toBe('enter'); //ngRepeat 4
+
+ $animate.flush();
+
+ expect(element.text()).toEqual('34');
+
+ function n(text) {
+ return text.replace(/\r\n/m, '').replace(/\r\n/m, '');
+ }
+ });
+ });
+
+ it('should destroy the previous leave animation if a new one takes place',
+ inject(function($compile, $rootScope, $animate, $location, $timeout) {
+ var $scope = $rootScope.$new();
+ element = $compile(html(
+ '
'
+ ))($scope);
+
+ $scope.$apply('value = true');
+
+ $location.path('/bar');
+ $rootScope.$digest();
+
+ var destroyed, inner = element.children(0);
+ inner.on('$destroy', function() {
+ destroyed = true;
+ });
+
+ $location.path('/foo');
+ $rootScope.$digest();
+
+ $location.path('/bar');
+ $rootScope.$digest();
+
+ $location.path('/bar');
+ $rootScope.$digest();
+
+ expect(destroyed).toBe(true);
+ })
+ );
+ });
+
+
+ describe('autoscroll', function() {
+ var autoScrollSpy;
+
+ function spyOnAnchorScroll() {
+ return function($provide, $routeProvider) {
+ autoScrollSpy = jasmine.createSpy('$anchorScroll');
+ $provide.value('$anchorScroll', autoScrollSpy);
+ $routeProvider.when('/foo', {
+ controller: angular.noop,
+ template: '
'
+ });
+ };
+ }
+
+ function spyOnAnimateEnter() {
+ return function($animate) {
+ spyOn($animate, 'enter').and.callThrough();
+ };
+ }
+
+ function compileAndLink(tpl) {
+ return function($compile, $rootScope, $location) {
+ element = $compile(tpl)($rootScope);
+ };
+ }
+
+ beforeEach(module(spyOnAnchorScroll(), 'ngAnimateMock'));
+ beforeEach(inject(spyOnAnimateEnter()));
+
+ it('should call $anchorScroll if autoscroll attribute is present', inject(
+ compileAndLink('
'),
+ function($rootScope, $animate, $timeout, $location) {
+
+ $location.path('/foo');
+ $rootScope.$digest();
+
+ $animate.flush();
+ $rootScope.$digest();
+
+ expect($animate.queue.shift().event).toBe('enter');
+ expect(autoScrollSpy).toHaveBeenCalledOnce();
+ }));
+
+
+ it('should call $anchorScroll if autoscroll evaluates to true', inject(
+ compileAndLink('
'),
+ function($rootScope, $animate, $timeout, $location) {
+
+ $rootScope.value = true;
+ $location.path('/foo');
+ $rootScope.$digest();
+
+ $animate.flush();
+ $rootScope.$digest();
+
+ expect($animate.queue.shift().event).toBe('enter');
+ expect(autoScrollSpy).toHaveBeenCalledOnce();
+ }));
+
+
+ it('should not call $anchorScroll if autoscroll attribute is not present', inject(
+ compileAndLink('
'),
+ function($rootScope, $location, $animate, $timeout) {
+
+ $location.path('/foo');
+ $rootScope.$digest();
+ expect($animate.queue.shift().event).toBe('enter');
+
+ expect(autoScrollSpy).not.toHaveBeenCalled();
+ }));
+
+
+ it('should not call $anchorScroll if autoscroll evaluates to false', inject(
+ compileAndLink('
'),
+ function($rootScope, $location, $animate, $timeout) {
+
+ $rootScope.value = false;
+ $location.path('/foo');
+ $rootScope.$digest();
+ expect($animate.queue.shift().event).toBe('enter');
+
+ expect(autoScrollSpy).not.toHaveBeenCalled();
+ }));
+
+
+ it('should only call $anchorScroll after the "enter" animation completes', inject(
+ compileAndLink('
'),
+ function($rootScope, $location, $animate, $timeout) {
+ $location.path('/foo');
+
+ expect($animate.enter).not.toHaveBeenCalled();
+ $rootScope.$digest();
+
+ expect(autoScrollSpy).not.toHaveBeenCalled();
+
+ expect($animate.queue.shift().event).toBe('enter');
+
+ $animate.flush();
+ $rootScope.$digest();
+
+ expect($animate.enter).toHaveBeenCalledOnce();
+ expect(autoScrollSpy).toHaveBeenCalledOnce();
+ }
+ ));
+ });
+ });
+
+ describe('in async template', function() {
+ beforeEach(module('ngRoute'));
+ beforeEach(module(function($compileProvider, $provide, $routeProvider) {
+ $compileProvider.directive('asyncView', function() {
+ return {templateUrl: 'async-view.html'};
+ });
+
+ $provide.decorator('$templateRequest', function($timeout) {
+ return function() {
+ return $timeout(angular.identity, 500, false, '
');
+ };
+ });
+
+ $routeProvider.when('/', {template: 'Hello, world!'});
+ }));
+
+
+ it('should work correctly upon initial page load',
+ // Injecting `$location` here is necessary, so that it gets instantiated early
+ inject(function($compile, $location, $rootScope, $timeout) {
+ var elem = $compile('
')($rootScope);
+ $rootScope.$digest();
+ $timeout.flush(500);
+
+ expect(elem.text()).toBe('Hello, world!');
+
+ dealoc(elem);
+ })
+ );
+ });
+});
+
+describe('$routeParams', function() {
+
+ beforeEach(module('ngRoute'));
+
+
+ it('should publish the params into a service', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {});
+ $routeProvider.when('/bar/:barId', {});
+ });
+
+ inject(function($rootScope, $route, $location, $routeParams) {
+ $location.path('/foo').search('a=b');
+ $rootScope.$digest();
+ expect($routeParams).toEqual({a:'b'});
+
+ $location.path('/bar/123').search('x=abc');
+ $rootScope.$digest();
+ expect($routeParams).toEqual({barId:'123', x:'abc'});
+ });
+ });
+
+ it('should correctly extract the params when a param name is part of the route', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/bar/:foo/:bar', {});
+ });
+
+ inject(function($rootScope, $route, $location, $routeParams) {
+ $location.path('/bar/foovalue/barvalue');
+ $rootScope.$digest();
+ expect($routeParams).toEqual({bar:'barvalue', foo:'foovalue'});
+ });
+ });
+
+ it('should support route params not preceded by slashes', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/bar:barId/foo:fooId/', {});
+ });
+
+ inject(function($rootScope, $route, $location, $routeParams) {
+ $location.path('/barbarvalue/foofoovalue/');
+ $rootScope.$digest();
+ expect($routeParams).toEqual({barId: 'barvalue', fooId: 'foovalue'});
+ });
+ });
+
+ it('should correctly extract the params when an optional param name is part of the route', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/bar/:foo?', {});
+ $routeProvider.when('/baz/:foo?/edit', {});
+ $routeProvider.when('/qux/:bar?/:baz?', {});
+ });
+
+ inject(function($rootScope, $route, $location, $routeParams) {
+ $location.path('/bar');
+ $rootScope.$digest();
+ expect($routeParams).toEqual({});
+
+ $location.path('/bar/fooValue');
+ $rootScope.$digest();
+ expect($routeParams).toEqual({foo: 'fooValue'});
+
+ $location.path('/baz/fooValue/edit');
+ $rootScope.$digest();
+ expect($routeParams).toEqual({foo: 'fooValue'});
+
+ $location.path('/baz/edit');
+ $rootScope.$digest();
+ expect($routeParams).toEqual({});
+
+ $location.path('/qux//bazValue');
+ $rootScope.$digest();
+ expect($routeParams).toEqual({baz: 'bazValue'});
+
+ });
+ });
+
+ it('should correctly extract path params containing hashes and/or question marks', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/foo/:bar', {});
+ $routeProvider.when('/zoo/:bar/:baz/:qux', {});
+ });
+
+ inject(function($location, $rootScope, $routeParams) {
+ $location.path('/foo/bar?baz');
+ $rootScope.$digest();
+ expect($routeParams).toEqual({bar: 'bar?baz'});
+
+ $location.path('/foo/bar?baz=val');
+ $rootScope.$digest();
+ expect($routeParams).toEqual({bar: 'bar?baz=val'});
+
+ $location.path('/foo/bar#baz');
+ $rootScope.$digest();
+ expect($routeParams).toEqual({bar: 'bar#baz'});
+
+ $location.path('/foo/bar?baz#qux');
+ $rootScope.$digest();
+ expect($routeParams).toEqual({bar: 'bar?baz#qux'});
+
+ $location.path('/foo/bar?baz=val#qux');
+ $rootScope.$digest();
+ expect($routeParams).toEqual({bar: 'bar?baz=val#qux'});
+
+ $location.path('/foo/bar#baz?qux');
+ $rootScope.$digest();
+ expect($routeParams).toEqual({bar: 'bar#baz?qux'});
+
+ $location.path('/zoo/bar?p1=v1#h1/baz?p2=v2#h2/qux?p3=v3#h3');
+ $rootScope.$digest();
+ expect($routeParams).toEqual({
+ bar: 'bar?p1=v1#h1',
+ baz: 'baz?p2=v2#h2',
+ qux: 'qux?p3=v3#h3'
+ });
+ });
+ });
+
+});
+
+describe('$routeProvider', function() {
+ var $routeProvider;
+
+ beforeEach(module('ngRoute'));
+ beforeEach(module(function(_$routeProvider_) {
+ $routeProvider = _$routeProvider_;
+ $routeProvider.when('/foo', {template: 'Hello, world!'});
+ }));
+
+
+ it('should support enabling/disabling automatic instantiation upon initial load',
+ inject(function() {
+ expect($routeProvider.eagerInstantiationEnabled(true)).toBe($routeProvider);
+ expect($routeProvider.eagerInstantiationEnabled()).toBe(true);
+
+ expect($routeProvider.eagerInstantiationEnabled(false)).toBe($routeProvider);
+ expect($routeProvider.eagerInstantiationEnabled()).toBe(false);
+
+ expect($routeProvider.eagerInstantiationEnabled(true)).toBe($routeProvider);
+ expect($routeProvider.eagerInstantiationEnabled()).toBe(true);
+ })
+ );
+
+
+ it('should automatically instantiate `$route` upon initial load', function() {
+ inject(function($location, $rootScope) {
+ $location.path('/foo');
+ $rootScope.$digest();
+ });
+
+ inject(function($route) {
+ expect($route.current).toBeDefined();
+ });
+ });
+
+
+ it('should not automatically instantiate `$route` if disabled', function() {
+ module(function($routeProvider) {
+ $routeProvider.eagerInstantiationEnabled(false);
+ });
+
+ inject(function($location, $rootScope) {
+ $location.path('/foo');
+ $rootScope.$digest();
+ });
+
+ inject(function($route) {
+ expect($route.current).toBeUndefined();
+ });
+ });
+});
+
+
+describe('$route', function() {
+ var $httpBackend,
+ element;
+
+ beforeEach(module('ngRoute'));
+
+ beforeEach(module(function() {
+ return function(_$httpBackend_) {
+ $httpBackend = _$httpBackend_;
+ $httpBackend.when('GET', 'Chapter.html').respond('chapter');
+ $httpBackend.when('GET', 'test.html').respond('test');
+ $httpBackend.when('GET', 'foo.html').respond('foo');
+ $httpBackend.when('GET', 'bar.html').respond('bar');
+ $httpBackend.when('GET', 'baz.html').respond('baz');
+ $httpBackend.when('GET', 'http://example.com/trusted-template.html').respond('cross domain trusted template');
+ $httpBackend.when('GET', '404.html').respond('not found');
+ };
+ }));
+
+ afterEach(function() {
+ dealoc(element);
+ });
+
+
+ it('should allow cancellation via $locationChangeStart via $routeChangeStart', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/Edit', {
+ id: 'edit', template: 'Some edit functionality'
+ });
+ $routeProvider.when('/Home', {
+ id: 'home'
+ });
+ });
+ module(provideLog);
+ inject(function($route, $location, $rootScope, $compile, log) {
+ $rootScope.$on('$routeChangeStart', function(event, next, current) {
+ if (next.id === 'home' && current.scope.unsavedChanges) {
+ event.preventDefault();
+ }
+ });
+ element = $compile('
')($rootScope);
+ $rootScope.$apply(function() {
+ $location.path('/Edit');
+ });
+ $rootScope.$on('$routeChangeSuccess', log.fn('routeChangeSuccess'));
+ $rootScope.$on('$locationChangeSuccess', log.fn('locationChangeSuccess'));
+
+ // aborted route change
+ $rootScope.$apply(function() {
+ $route.current.scope.unsavedChanges = true;
+ });
+ $rootScope.$apply(function() {
+ $location.path('/Home');
+ });
+ expect($route.current.id).toBe('edit');
+ expect($location.path()).toBe('/Edit');
+ expect(log).toEqual([]);
+
+ // successful route change
+ $rootScope.$apply(function() {
+ $route.current.scope.unsavedChanges = false;
+ });
+ $rootScope.$apply(function() {
+ $location.path('/Home');
+ });
+ expect($route.current.id).toBe('home');
+ expect($location.path()).toBe('/Home');
+ expect(log).toEqual(['locationChangeSuccess', 'routeChangeSuccess']);
+ });
+ });
+
+ it('should allow redirects while handling $routeChangeStart', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/some', {
+ id: 'some', template: 'Some functionality'
+ });
+ $routeProvider.when('/redirect', {
+ id: 'redirect'
+ });
+ });
+ module(provideLog);
+ inject(function($route, $location, $rootScope, $compile, log) {
+ $rootScope.$on('$routeChangeStart', function(event, next, current) {
+ if (next.id === 'some') {
+ $location.path('/redirect');
+ }
+ });
+ $compile('
')($rootScope);
+ $rootScope.$on('$routeChangeStart', log.fn('routeChangeStart'));
+ $rootScope.$on('$routeChangeError', log.fn('routeChangeError'));
+ $rootScope.$on('$routeChangeSuccess', log.fn('routeChangeSuccess'));
+ $rootScope.$apply(function() {
+ $location.path('/some');
+ });
+
+ expect($route.current.id).toBe('redirect');
+ expect($location.path()).toBe('/redirect');
+ expect(log).toEqual(['routeChangeStart', 'routeChangeStart', 'routeChangeSuccess']);
+ });
+ });
+
+ it('should route and fire change event', function() {
+ var log = '',
+ lastRoute,
+ nextRoute;
+
+ module(function($routeProvider) {
+ $routeProvider.when('/Book/:book/Chapter/:chapter',
+ {controller: angular.noop, templateUrl: 'Chapter.html'});
+ $routeProvider.when('/Blank', {});
+ });
+ inject(function($route, $location, $rootScope) {
+ $rootScope.$on('$routeChangeStart', function(event, next, current) {
+ log += 'before();';
+ expect(current).toBe($route.current);
+ lastRoute = current;
+ nextRoute = next;
+ });
+ $rootScope.$on('$routeChangeSuccess', function(event, current, last) {
+ log += 'after();';
+ expect(current).toBe($route.current);
+ expect(lastRoute).toBe(last);
+ expect(nextRoute).toBe(current);
+ });
+
+ $location.path('/Book/Moby/Chapter/Intro').search('p=123');
+ $rootScope.$digest();
+ $httpBackend.flush();
+ expect(log).toEqual('before();after();');
+ expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', p:'123'});
+
+ log = '';
+ $location.path('/Blank').search('ignore');
+ $rootScope.$digest();
+ expect(log).toEqual('before();after();');
+ expect($route.current.params).toEqual({ignore:true});
+
+ log = '';
+ $location.path('/NONE');
+ $rootScope.$digest();
+ expect(log).toEqual('before();after();');
+ expect($route.current).toEqual(undefined);
+ });
+ });
+
+ it('should route and fire change event when catch-all params are used', function() {
+ var log = '',
+ lastRoute,
+ nextRoute;
+
+ module(function($routeProvider) {
+ $routeProvider.when('/Book1/:book/Chapter/:chapter/:highlight*/edit',
+ {controller: angular.noop, templateUrl: 'Chapter.html'});
+ $routeProvider.when('/Book2/:book/:highlight*/Chapter/:chapter',
+ {controller: angular.noop, templateUrl: 'Chapter.html'});
+ $routeProvider.when('/Blank', {});
+ });
+ inject(function($route, $location, $rootScope) {
+ $rootScope.$on('$routeChangeStart', function(event, next, current) {
+ log += 'before();';
+ expect(current).toBe($route.current);
+ lastRoute = current;
+ nextRoute = next;
+ });
+ $rootScope.$on('$routeChangeSuccess', function(event, current, last) {
+ log += 'after();';
+ expect(current).toBe($route.current);
+ expect(lastRoute).toBe(last);
+ expect(nextRoute).toBe(current);
+ });
+
+ $location.path('/Book1/Moby/Chapter/Intro/one/edit').search('p=123');
+ $rootScope.$digest();
+ $httpBackend.flush();
+ expect(log).toEqual('before();after();');
+ expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', highlight:'one', p:'123'});
+
+ log = '';
+ $location.path('/Blank').search('ignore');
+ $rootScope.$digest();
+ expect(log).toEqual('before();after();');
+ expect($route.current.params).toEqual({ignore:true});
+
+ log = '';
+ $location.path('/Book1/Moby/Chapter/Intro/one/two/edit').search('p=123');
+ $rootScope.$digest();
+ expect(log).toEqual('before();after();');
+ expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', highlight:'one/two', p:'123'});
+
+ log = '';
+ $location.path('/Book2/Moby/one/two/Chapter/Intro').search('p=123');
+ $rootScope.$digest();
+ expect(log).toEqual('before();after();');
+ expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', highlight:'one/two', p:'123'});
+
+ log = '';
+ $location.path('/NONE');
+ $rootScope.$digest();
+ expect(log).toEqual('before();after();');
+ expect($route.current).toEqual(undefined);
+ });
+ });
+
+
+ it('should route and fire change event correctly whenever the case insensitive flag is utilized', function() {
+ var log = '',
+ lastRoute,
+ nextRoute;
+
+ module(function($routeProvider) {
+ $routeProvider.when('/Book1/:book/Chapter/:chapter/:highlight*/edit',
+ {controller: angular.noop, templateUrl: 'Chapter.html', caseInsensitiveMatch: true});
+ $routeProvider.when('/Book2/:book/:highlight*/Chapter/:chapter',
+ {controller: angular.noop, templateUrl: 'Chapter.html'});
+ $routeProvider.when('/Blank', {});
+ });
+ inject(function($route, $location, $rootScope) {
+ $rootScope.$on('$routeChangeStart', function(event, next, current) {
+ log += 'before();';
+ expect(current).toBe($route.current);
+ lastRoute = current;
+ nextRoute = next;
+ });
+ $rootScope.$on('$routeChangeSuccess', function(event, current, last) {
+ log += 'after();';
+ expect(current).toBe($route.current);
+ expect(lastRoute).toBe(last);
+ expect(nextRoute).toBe(current);
+ });
+
+ $location.path('/Book1/Moby/Chapter/Intro/one/edit').search('p=123');
+ $rootScope.$digest();
+ $httpBackend.flush();
+ expect(log).toEqual('before();after();');
+ expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', highlight:'one', p:'123'});
+
+ log = '';
+ $location.path('/BOOK1/Moby/CHAPTER/Intro/one/EDIT').search('p=123');
+ $rootScope.$digest();
+ expect(log).toEqual('before();after();');
+ expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', highlight:'one', p:'123'});
+
+ log = '';
+ $location.path('/Blank').search('ignore');
+ $rootScope.$digest();
+ expect(log).toEqual('before();after();');
+ expect($route.current.params).toEqual({ignore:true});
+
+ log = '';
+ $location.path('/BLANK');
+ $rootScope.$digest();
+ expect(log).toEqual('before();after();');
+ expect($route.current).toEqual(undefined);
+
+ log = '';
+ $location.path('/Book2/Moby/one/two/Chapter/Intro').search('p=123');
+ $rootScope.$digest();
+ expect(log).toEqual('before();after();');
+ expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', highlight:'one/two', p:'123'});
+
+ log = '';
+ $location.path('/BOOK2/Moby/one/two/CHAPTER/Intro').search('p=123');
+ $rootScope.$digest();
+ expect(log).toEqual('before();after();');
+ expect($route.current).toEqual(undefined);
+ });
+ });
+
+ it('should allow configuring caseInsensitiveMatch on the route provider level', function() {
+ module(function($routeProvider) {
+ $routeProvider.caseInsensitiveMatch = true;
+ $routeProvider.when('/Blank', {template: 'blank'});
+ $routeProvider.otherwise({template: 'other'});
+ });
+ inject(function($route, $location, $rootScope) {
+ $location.path('/bLaNk');
+ $rootScope.$digest();
+ expect($route.current.template).toBe('blank');
+ });
+ });
+
+ it('should allow overriding provider\'s caseInsensitiveMatch setting on the route level', function() {
+ module(function($routeProvider) {
+ $routeProvider.caseInsensitiveMatch = true;
+ $routeProvider.when('/Blank', {template: 'blank', caseInsensitiveMatch: false});
+ $routeProvider.otherwise({template: 'other'});
+ });
+ inject(function($route, $location, $rootScope) {
+ $location.path('/bLaNk');
+ $rootScope.$digest();
+ expect($route.current.template).toBe('other');
+ });
+ });
+
+ it('should not change route when location is canceled', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/somePath', {template: 'some path'});
+ });
+ inject(function($route, $location, $rootScope, $log) {
+ $rootScope.$on('$locationChangeStart', function(event) {
+ $log.info('$locationChangeStart');
+ event.preventDefault();
+ });
+
+ $rootScope.$on('$routeChangeSuccess', function(event) {
+ throw new Error('Should not get here');
+ });
+
+ $location.path('/somePath');
+ $rootScope.$digest();
+
+ expect($log.info.logs.shift()).toEqual(['$locationChangeStart']);
+ });
+ });
+
+
+ describe('should match a route that contains special chars in the path', function() {
+ beforeEach(module(function($routeProvider) {
+ $routeProvider.when('/$test.23/foo*(bar)/:baz', {templateUrl: 'test.html'});
+ }));
+
+ it('matches the full path', inject(function($route, $location, $rootScope) {
+ $location.path('/test');
+ $rootScope.$digest();
+ expect($route.current).toBeUndefined();
+ }));
+
+ it('matches literal .', inject(function($route, $location, $rootScope) {
+ $location.path('/$testX23/foo*(bar)/222');
+ $rootScope.$digest();
+ expect($route.current).toBeUndefined();
+ }));
+
+ it('matches literal *', inject(function($route, $location, $rootScope) {
+ $location.path('/$test.23/foooo(bar)/222');
+ $rootScope.$digest();
+ expect($route.current).toBeUndefined();
+ }));
+
+ it('treats backslashes normally', inject(function($route, $location, $rootScope) {
+ $location.path('/$test.23/foo*\\(bar)/222');
+ $rootScope.$digest();
+ expect($route.current).toBeUndefined();
+ }));
+
+ it('matches a URL with special chars', inject(function($route, $location, $rootScope) {
+ $location.path('/$test.23/foo*(bar)/~!@#$%^&*()_+=-`');
+ $rootScope.$digest();
+ expect($route.current).toBeDefined();
+ }));
+
+ it('should use route params inherited from prototype chain', function() {
+ function BaseRoute() {}
+ BaseRoute.prototype.templateUrl = 'foo.html';
+
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', new BaseRoute());
+ });
+
+ inject(function($route, $location, $rootScope) {
+ $location.path('/foo');
+ $rootScope.$digest();
+ expect($route.current.templateUrl).toBe('foo.html');
+ });
+ });
+ });
+
+
+ describe('should match a route that contains optional params in the path', function() {
+ beforeEach(module(function($routeProvider) {
+ $routeProvider.when('/test/:opt?/:baz/edit', {templateUrl: 'test.html'});
+ }));
+
+ it('matches a URL with optional params', inject(function($route, $location, $rootScope) {
+ $location.path('/test/optValue/bazValue/edit');
+ $rootScope.$digest();
+ expect($route.current).toBeDefined();
+ }));
+
+ it('matches a URL without optional param', inject(function($route, $location, $rootScope) {
+ $location.path('/test//bazValue/edit');
+ $rootScope.$digest();
+ expect($route.current).toBeDefined();
+ }));
+
+ it('not match a URL with a required param', inject(function($route, $location, $rootScope) {
+ $location.path('///edit');
+ $rootScope.$digest();
+ expect($route.current).not.toBeDefined();
+ }));
+ });
+
+
+ it('should change route even when only search param changes', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/test', {templateUrl: 'test.html'});
+ });
+
+ inject(function($route, $location, $rootScope) {
+ var callback = jasmine.createSpy('onRouteChange');
+
+ $rootScope.$on('$routeChangeStart', callback);
+ $location.path('/test');
+ $rootScope.$digest();
+ callback.calls.reset();
+
+ $location.search({any: true});
+ $rootScope.$digest();
+
+ expect(callback).toHaveBeenCalled();
+ });
+ });
+
+
+ it('should allow routes to be defined with just templates without controllers', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {templateUrl: 'foo.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('/foo');
+ $rootScope.$digest();
+
+ expect($route.current.templateUrl).toEqual('foo.html');
+ expect($route.current.controller).toBeUndefined();
+ expect(onChangeSpy).toHaveBeenCalled();
+ });
+ });
+
+
+ it('should chain whens and otherwise', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {templateUrl: 'foo.html'}).
+ otherwise({templateUrl: 'bar.html'}).
+ when('/baz', {templateUrl: 'baz.html'});
+ });
+
+ inject(function($route, $location, $rootScope) {
+ $rootScope.$digest();
+ expect($route.current.templateUrl).toBe('bar.html');
+
+ $location.url('https://github.com/baz');
+ $rootScope.$digest();
+ expect($route.current.templateUrl).toBe('baz.html');
+ });
+ });
+
+
+ it('should skip routes with incomplete params', function() {
+ module(function($routeProvider) {
+ $routeProvider
+ .otherwise({template: 'other'})
+ .when('/pages/:page/:comment*', {template: 'comment'})
+ .when('/pages/:page', {template: 'page'})
+ .when('/pages', {template: 'index'})
+ .when('/foo/', {template: 'foo'})
+ .when('/foo/:bar', {template: 'bar'})
+ .when('/foo/:bar*/:baz', {template: 'baz'});
+ });
+
+ inject(function($route, $location, $rootScope) {
+ $location.url('https://github.com/pages/');
+ $rootScope.$digest();
+ expect($route.current.template).toBe('index');
+
+ $location.url('https://github.com/pages/page/');
+ $rootScope.$digest();
+ expect($route.current.template).toBe('page');
+
+ $location.url('https://github.com/pages/page/1/');
+ $rootScope.$digest();
+ expect($route.current.template).toBe('comment');
+
+ $location.url('https://github.com/foo/');
+ $rootScope.$digest();
+ expect($route.current.template).toBe('foo');
+
+ $location.url('https://github.com/foo/bar/');
+ $rootScope.$digest();
+ expect($route.current.template).toBe('bar');
+
+ $location.url('https://github.com/foo/bar/baz/');
+ $rootScope.$digest();
+ expect($route.current.template).toBe('baz');
+
+ $location.url('https://github.com/something/');
+ $rootScope.$digest();
+ expect($route.current.template).toBe('other');
+ });
+ });
+
+
+ describe('otherwise', function() {
+
+ it('should handle unknown routes with "otherwise" route definition', function() {
+ function NotFoundCtrl() {}
+
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {templateUrl: 'foo.html'});
+ $routeProvider.otherwise({templateUrl: '404.html', controller: NotFoundCtrl});
+ });
+
+ inject(function($route, $location, $rootScope) {
+ var onChangeSpy = jasmine.createSpy('onChange');
+
+ $rootScope.$on('$routeChangeStart', onChangeSpy);
+ expect($route.current).toBeUndefined();
+ expect(onChangeSpy).not.toHaveBeenCalled();
+
+ $location.path('/unknownRoute');
+ $rootScope.$digest();
+
+ expect($route.current.templateUrl).toBe('404.html');
+ expect($route.current.controller).toBe(NotFoundCtrl);
+ expect(onChangeSpy).toHaveBeenCalled();
+
+ onChangeSpy.calls.reset();
+ $location.path('/foo');
+ $rootScope.$digest();
+
+ expect($route.current.templateUrl).toEqual('foo.html');
+ expect($route.current.controller).toBeUndefined();
+ expect(onChangeSpy).toHaveBeenCalled();
+ });
+ });
+
+
+ it('should update $route.current and $route.next when default route is matched', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {templateUrl: 'foo.html'});
+ $routeProvider.otherwise({templateUrl: '404.html'});
+ });
+
+ inject(function($route, $location, $rootScope) {
+ var currentRoute, nextRoute,
+ onChangeSpy = jasmine.createSpy('onChange').and.callFake(function(e, next) {
+ currentRoute = $route.current;
+ nextRoute = next;
+ });
+
+
+ // init
+ $rootScope.$on('$routeChangeStart', onChangeSpy);
+ expect($route.current).toBeUndefined();
+ expect(onChangeSpy).not.toHaveBeenCalled();
+
+
+ // match otherwise route
+ $location.path('/unknownRoute');
+ $rootScope.$digest();
+
+ expect(currentRoute).toBeUndefined();
+ expect(nextRoute.templateUrl).toBe('404.html');
+ expect($route.current.templateUrl).toBe('404.html');
+ expect(onChangeSpy).toHaveBeenCalled();
+ onChangeSpy.calls.reset();
+
+ // match regular route
+ $location.path('/foo');
+ $rootScope.$digest();
+
+ expect(currentRoute.templateUrl).toBe('404.html');
+ expect(nextRoute.templateUrl).toBe('foo.html');
+ expect($route.current.templateUrl).toEqual('foo.html');
+ expect(onChangeSpy).toHaveBeenCalled();
+ onChangeSpy.calls.reset();
+
+ // match otherwise route again
+ $location.path('/anotherUnknownRoute');
+ $rootScope.$digest();
+
+ expect(currentRoute.templateUrl).toBe('foo.html');
+ expect(nextRoute.templateUrl).toBe('404.html');
+ expect($route.current.templateUrl).toEqual('404.html');
+ expect(onChangeSpy).toHaveBeenCalled();
+ });
+ });
+
+
+ it('should interpret a string as a redirect route', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {templateUrl: 'foo.html'});
+ $routeProvider.when('/baz', {templateUrl: 'baz.html'});
+ $routeProvider.otherwise('/foo');
+ });
+
+ inject(function($route, $location, $rootScope) {
+ $location.path('/unknownRoute');
+ $rootScope.$digest();
+
+ expect($location.path()).toBe('/foo');
+ expect($route.current.templateUrl).toBe('foo.html');
+ });
+ });
+ });
+
+
+ describe('events', function() {
+ it('should not fire $routeChangeStart/Success during bootstrap (if no route)', function() {
+ var routeChangeSpy = jasmine.createSpy('route change');
+
+ module(function($routeProvider) {
+ $routeProvider.when('/one', {}); // no otherwise defined
+ });
+
+ inject(function($rootScope, $route, $location) {
+ $rootScope.$on('$routeChangeStart', routeChangeSpy);
+ $rootScope.$on('$routeChangeSuccess', routeChangeSpy);
+
+ $rootScope.$digest();
+ expect(routeChangeSpy).not.toHaveBeenCalled();
+
+ $location.path('/no-route-here');
+ $rootScope.$digest();
+ expect(routeChangeSpy).not.toHaveBeenCalled();
+
+ $location.path('/one');
+ $rootScope.$digest();
+ expect(routeChangeSpy).toHaveBeenCalled();
+ });
+ });
+
+ it('should fire $routeChangeStart and resolve promises', function() {
+ var deferA,
+ deferB;
+
+ module(function($provide, $routeProvider) {
+ $provide.factory('b', function($q) {
+ deferB = $q.defer();
+ return deferB.promise;
+ });
+ $routeProvider.when('/path', { templateUrl: 'foo.html', resolve: {
+ a: ['$q', function($q) {
+ deferA = $q.defer();
+ return deferA.promise;
+ }],
+ b: 'b'
+ } });
+ });
+
+ inject(function($location, $route, $rootScope, $httpBackend) {
+ var log = '';
+
+ $httpBackend.expectGET('foo.html').respond('FOO');
+
+ $location.path('/path');
+ $rootScope.$digest();
+ expect(log).toEqual('');
+ $httpBackend.flush();
+ expect(log).toEqual('');
+ deferA.resolve();
+ $rootScope.$digest();
+ expect(log).toEqual('');
+ deferB.resolve();
+ $rootScope.$digest();
+ expect($route.current.locals.$template).toEqual('FOO');
+ });
+ });
+
+
+ it('should fire $routeChangeError event on resolution error', function() {
+ var deferA;
+
+ module(function($provide, $routeProvider) {
+ $routeProvider.when('/path', { template: 'foo', resolve: {
+ a: function($q) {
+ deferA = $q.defer();
+ return deferA.promise;
+ }
+ } });
+ });
+
+ inject(function($location, $route, $rootScope) {
+ var log = '';
+
+ $rootScope.$on('$routeChangeStart', function() { log += 'before();'; });
+ $rootScope.$on('$routeChangeError', function(e, n, l, reason) { log += 'failed(' + reason + ');'; });
+
+ $location.path('/path');
+ $rootScope.$digest();
+ expect(log).toEqual('before();');
+
+ deferA.reject('MyError');
+ $rootScope.$digest();
+ expect(log).toEqual('before();failed(MyError);');
+ });
+ });
+
+
+ it('should fetch templates', function() {
+ module(function($routeProvider) {
+ $routeProvider.
+ when('/r1', { templateUrl: 'r1.html' }).
+ when('/r2', { templateUrl: 'r2.html' });
+ });
+
+ inject(function($route, $httpBackend, $location, $rootScope) {
+ var log = '';
+ $rootScope.$on('$routeChangeStart', function(e, next) { log += '$before(' + next.templateUrl + ');'; });
+ $rootScope.$on('$routeChangeSuccess', function(e, next) { log += '$after(' + next.templateUrl + ');'; });
+
+ $httpBackend.expectGET('r1.html').respond('R1');
+ $httpBackend.expectGET('r2.html').respond('R2');
+
+ $location.path('/r1');
+ $rootScope.$digest();
+ expect(log).toBe('$before(r1.html);');
+
+ $location.path('/r2');
+ $rootScope.$digest();
+ expect(log).toBe('$before(r1.html);$before(r2.html);');
+
+ $httpBackend.flush();
+ expect(log).toBe('$before(r1.html);$before(r2.html);$after(r2.html);');
+ expect(log).not.toContain('$after(r1.html);');
+ });
+ });
+
+ it('should NOT load cross domain templates by default', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', { templateUrl: 'http://example.com/foo.html' });
+ });
+
+ inject(function($route, $location, $rootScope) {
+ var onError = jasmine.createSpy('onError');
+ var onSuccess = jasmine.createSpy('onSuccess');
+
+ $rootScope.$on('$routeChangeError', onError);
+ $rootScope.$on('$routeChangeSuccess', onSuccess);
+
+ $location.path('/foo');
+ $rootScope.$digest();
+
+ expect(onSuccess).not.toHaveBeenCalled();
+ expect(onError).toHaveBeenCalled();
+ expect(onError.calls.mostRecent().args[3]).toEqualMinErr('$sce', 'insecurl',
+ 'Blocked loading resource from url not allowed by $sceDelegate policy. ' +
+ 'URL: http://example.com/foo.html');
+ });
+ });
+
+ it('should load cross domain templates that are trusted', function() {
+ module(function($routeProvider, $sceDelegateProvider) {
+ $routeProvider.when('/foo', { templateUrl: 'http://example.com/foo.html' });
+ $sceDelegateProvider.trustedResourceUrlList([/^http:\/\/example\.com\/foo\.html$/]);
+ });
+
+ inject(function($route, $location, $rootScope) {
+ $httpBackend.whenGET('http://example.com/foo.html').respond('FOO BODY');
+ $location.path('/foo');
+ $rootScope.$digest();
+ $httpBackend.flush();
+ expect($route.current.locals.$template).toEqual('FOO BODY');
+ });
+ });
+
+ it('should not update $routeParams until $routeChangeSuccess', function() {
+ module(function($routeProvider) {
+ $routeProvider.
+ when('/r1/:id', { templateUrl: 'r1.html' }).
+ when('/r2/:id', { templateUrl: 'r2.html' });
+ });
+
+ inject(function($route, $httpBackend, $location, $rootScope, $routeParams) {
+ var log = '';
+ $rootScope.$on('$routeChangeStart', function(e, next) { log += '$before' + angular.toJson($routeParams) + ';'; });
+ $rootScope.$on('$routeChangeSuccess', function(e, next) { log += '$after' + angular.toJson($routeParams) + ';'; });
+
+ $httpBackend.whenGET('r1.html').respond('R1');
+ $httpBackend.whenGET('r2.html').respond('R2');
+
+ $location.path('/r1/1');
+ $rootScope.$digest();
+ expect(log).toBe('$before{};');
+ $httpBackend.flush();
+ expect(log).toBe('$before{};$after{"id":"1"};');
+
+ log = '';
+
+ $location.path('/r2/2');
+ $rootScope.$digest();
+ expect(log).toBe('$before{"id":"1"};');
+ $httpBackend.flush();
+ expect(log).toBe('$before{"id":"1"};$after{"id":"2"};');
+ });
+ });
+
+
+ it('should drop in progress route change when new route change occurs', function() {
+ module(function($routeProvider) {
+ $routeProvider.
+ when('/r1', { templateUrl: 'r1.html' }).
+ when('/r2', { templateUrl: 'r2.html' });
+ });
+
+ inject(function($route, $httpBackend, $location, $rootScope) {
+ var log = '';
+ $rootScope.$on('$routeChangeStart', function(e, next) { log += '$before(' + next.templateUrl + ');'; });
+ $rootScope.$on('$routeChangeSuccess', function(e, next) { log += '$after(' + next.templateUrl + ');'; });
+
+ $httpBackend.expectGET('r1.html').respond('R1');
+ $httpBackend.expectGET('r2.html').respond('R2');
+
+ $location.path('/r1');
+ $rootScope.$digest();
+ expect(log).toBe('$before(r1.html);');
+
+ $location.path('/r2');
+ $rootScope.$digest();
+ expect(log).toBe('$before(r1.html);$before(r2.html);');
+
+ $httpBackend.flush();
+ expect(log).toBe('$before(r1.html);$before(r2.html);$after(r2.html);');
+ expect(log).not.toContain('$after(r1.html);');
+ });
+ });
+
+
+ it('should throw an error when a template is not found', function() {
+ module(function($routeProvider, $exceptionHandlerProvider) {
+ $exceptionHandlerProvider.mode('log');
+ $routeProvider.
+ when('/r1', { templateUrl: 'r1.html' }).
+ when('/r2', { templateUrl: 'r2.html' }).
+ when('/r3', { templateUrl: 'r3.html' });
+ });
+
+ inject(function($route, $httpBackend, $location, $rootScope, $exceptionHandler) {
+ $httpBackend.expectGET('r1.html').respond(404, 'R1');
+ $location.path('/r1');
+ $rootScope.$digest();
+
+ $httpBackend.flush();
+ expect($exceptionHandler.errors.pop()).
+ toEqualMinErr('$templateRequest', 'tpload', 'Failed to load template: r1.html');
+
+ $httpBackend.expectGET('r2.html').respond('');
+ $location.path('/r2');
+ $rootScope.$digest();
+
+ $httpBackend.flush();
+ expect($exceptionHandler.errors.length).toBe(0);
+
+ $httpBackend.expectGET('r3.html').respond('abc');
+ $location.path('/r3');
+ $rootScope.$digest();
+
+ $httpBackend.flush();
+ expect($exceptionHandler.errors.length).toBe(0);
+ });
+ });
+
+
+ it('should catch local factory errors', function() {
+ var myError = new Error('MyError');
+ module(function($routeProvider) {
+ $routeProvider.when('/locals', {
+ resolve: {
+ a: function($q) {
+ throw myError;
+ }
+ }
+ });
+ });
+
+ inject(function($location, $route, $rootScope) {
+ spyOn($rootScope, '$broadcast').and.callThrough();
+
+ $location.path('/locals');
+ $rootScope.$digest();
+
+ expect($rootScope.$broadcast).toHaveBeenCalledWith(
+ '$routeChangeError', jasmine.any(Object), undefined, myError);
+ });
+ });
+ });
+
+
+ it('should match route with and without trailing slash', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {templateUrl: 'foo.html'});
+ $routeProvider.when('/bar/', {templateUrl: 'bar.html'});
+ });
+
+ inject(function($route, $location, $rootScope) {
+ $location.path('/foo');
+ $rootScope.$digest();
+ expect($location.path()).toBe('/foo');
+ expect($route.current.templateUrl).toBe('foo.html');
+
+ $location.path('/foo/');
+ $rootScope.$digest();
+ expect($location.path()).toBe('/foo');
+ expect($route.current.templateUrl).toBe('foo.html');
+
+ $location.path('/bar');
+ $rootScope.$digest();
+ expect($location.path()).toBe('/bar/');
+ expect($route.current.templateUrl).toBe('bar.html');
+
+ $location.path('/bar/');
+ $rootScope.$digest();
+ expect($location.path()).toBe('/bar/');
+ expect($route.current.templateUrl).toBe('bar.html');
+ });
+ });
+
+
+ it('should not get affected by modifying the route definition object after route registration',
+ function() {
+ module(function($routeProvider) {
+ var rdo = {};
+
+ rdo.templateUrl = 'foo.html';
+ $routeProvider.when('/foo', rdo);
+
+ rdo.templateUrl = 'bar.html';
+ $routeProvider.when('/bar', rdo);
+ });
+
+ inject(function($location, $rootScope, $route) {
+ $location.path('/bar');
+ $rootScope.$digest();
+ expect($location.path()).toBe('/bar');
+ expect($route.current.templateUrl).toBe('bar.html');
+
+ $location.path('/foo');
+ $rootScope.$digest();
+ expect($location.path()).toBe('/foo');
+ expect($route.current.templateUrl).toBe('foo.html');
+ });
+ }
+ );
+
+
+ it('should use the property values of the passed in route definition object directly',
+ function() {
+ var $routeProvider;
+
+ module(function(_$routeProvider_) {
+ $routeProvider = _$routeProvider_;
+ });
+
+ inject(function($location, $rootScope, $route, $sce) {
+ var sceWrappedUrl = $sce.trustAsResourceUrl('foo.html');
+ $routeProvider.when('/foo', {templateUrl: sceWrappedUrl});
+
+ $location.path('/foo');
+ $rootScope.$digest();
+ expect($location.path()).toBe('/foo');
+ expect($route.current.templateUrl).toBe(sceWrappedUrl);
+ });
+ }
+ );
+
+
+ it('should support custom `$sce` implementations', function() {
+ function MySafeResourceUrl(val) {
+ var self = this;
+ this._val = val;
+ this.getVal = function() {
+ return (this !== self) ? null : this._val;
+ };
+ }
+
+ var $routeProvider;
+
+ module(function($provide, _$routeProvider_) {
+ $routeProvider = _$routeProvider_;
+
+ $provide.decorator('$sce', function($delegate) {
+ function getVal(v) { return v.getVal ? v.getVal() : v; }
+ $delegate.trustAsResourceUrl = function(url) { return new MySafeResourceUrl(url); };
+ $delegate.getTrustedResourceUrl = function(v) { return getVal(v); };
+ $delegate.valueOf = function(v) { return getVal(v); };
+ return $delegate;
+ });
+ });
+
+ inject(function($location, $rootScope, $route, $sce) {
+ $routeProvider.when('/foo', {templateUrl: $sce.trustAsResourceUrl('foo.html')});
+
+ $location.path('/foo');
+ $rootScope.$digest();
+ expect($location.path()).toBe('/foo');
+ expect($sce.valueOf($route.current.templateUrl)).toBe('foo.html');
+ });
+ });
+
+
+ describe('redirection', function() {
+ 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);
+ });
+ });
+
+
+ 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'});
+ });
+
+ 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');
+
+ $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 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');
+ });
+ });
+
+
+ it('should properly process route params which are both eager and optional', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/foo/:param1*?/:param2', {templateUrl: '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 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');
+ });
+ });
+
+
+ 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 broadcast `$routeChangeError` when redirectTo throws', function() {
+ var error = new Error('Test');
+
+ module(function($routeProvider) {
+ $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);
+ });
+ });
+
+
+ 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/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', 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;
+
+ 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();
+ });
+ });
+
+
+ it('should redirect to the returned url', function() {
+ module(function($routeProvider) {
+ $routeProvider.
+ when('/foo', {resolveRedirectTo: function() { return '/bar?baz=qux'; }}).
+ when('/bar', {template: 'Bar'});
+ });
+
+ inject(function() {
+ $location.path('/foo');
+ $rootScope.$digest();
+
+ expect($location.path()).toBe('/bar');
+ expect($location.search()).toEqual({baz: 'qux'});
+ expect($route.current.template).toBe('Bar');
+ });
+ });
+
+
+ it('should support returning a promise', function() {
+ module(function($routeProvider) {
+ $routeProvider.
+ when('/foo', {resolveRedirectTo: function($q) { return $q.resolve('/bar'); }}).
+ when('/bar', {template: 'Bar'});
+ });
+
+ inject(function() {
+ $location.path('/foo');
+ $rootScope.$digest();
+
+ expect($location.path()).toBe('/bar');
+ expect($route.current.template).toBe('Bar');
+ });
+ });
+
+
+ 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 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');
+ });
+ });
+
+
+ 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 redirect if `undefined` is returned', function() {
+ var spies = createSpies();
+ var called = false;
+
+ module(function($routeProvider) {
+ setupRoutes($routeProvider, spies, function() {
+ called = true;
+ return undefined;
+ });
+ });
+
+ 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);
+ });
+ });
+
+
+ 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();
+
+ 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 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);
+ });
+ });
+
+
+ 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
+ });
+ }
+ });
+ });
+
+
+ describe('reloadOnUrl', function() {
+ it('should reload when `reloadOnUrl` is true and `.url()` changes', function() {
+ var routeChange = jasmine.createSpy('routeChange');
+
+ module(function($routeProvider) {
+ $routeProvider.when('/path/:param', {});
+ });
+
+ inject(function($location, $rootScope, $routeParams) {
+ $rootScope.$on('$routeChangeStart', routeChange);
+
+ // Initial load
+ $location.path('/path/foo');
+ $rootScope.$digest();
+ expect(routeChange).toHaveBeenCalledOnce();
+ expect($routeParams).toEqual({param: 'foo'});
+
+ routeChange.calls.reset();
+
+ // Reload on `path` change
+ $location.path('/path/bar');
+ $rootScope.$digest();
+ expect(routeChange).toHaveBeenCalledOnce();
+ expect($routeParams).toEqual({param: 'bar'});
+
+ routeChange.calls.reset();
+
+ // Reload on `search` change
+ $location.search('foo', 'bar');
+ $rootScope.$digest();
+ expect(routeChange).toHaveBeenCalledOnce();
+ expect($routeParams).toEqual({param: 'bar', foo: 'bar'});
+
+ routeChange.calls.reset();
+
+ // Reload on `hash` change
+ $location.hash('baz');
+ $rootScope.$digest();
+ expect(routeChange).toHaveBeenCalledOnce();
+ expect($routeParams).toEqual({param: 'bar', foo: 'bar'});
+ });
+ });
+
+
+ it('should reload when `reloadOnUrl` is false and URL maps to different route',
+ function() {
+ var routeChange = jasmine.createSpy('routeChange');
+ var routeUpdate = jasmine.createSpy('routeUpdate');
+
+ module(function($routeProvider) {
+ $routeProvider.
+ when('/path/:param', {reloadOnUrl: false}).
+ otherwise({});
+ });
+
+ inject(function($location, $rootScope, $routeParams) {
+ $rootScope.$on('$routeChangeStart', routeChange);
+ $rootScope.$on('$routeChangeSuccess', routeChange);
+ $rootScope.$on('$routeUpdate', routeUpdate);
+
+ expect(routeChange).not.toHaveBeenCalled();
+
+ // Initial load
+ $location.path('/path/foo');
+ $rootScope.$digest();
+ expect(routeChange).toHaveBeenCalledTimes(2);
+ expect(routeUpdate).not.toHaveBeenCalled();
+ expect($routeParams).toEqual({param: 'foo'});
+
+ routeChange.calls.reset();
+
+ // Route change
+ $location.path('/other/path/bar');
+ $rootScope.$digest();
+ expect(routeChange).toHaveBeenCalledTimes(2);
+ expect(routeUpdate).not.toHaveBeenCalled();
+ expect($routeParams).toEqual({});
+ });
+ }
+ );
+
+
+ it('should not reload when `reloadOnUrl` is false and URL maps to the same route',
+ function() {
+ var routeChange = jasmine.createSpy('routeChange');
+ var routeUpdate = jasmine.createSpy('routeUpdate');
+
+ module(function($routeProvider) {
+ $routeProvider.when('/path/:param', {reloadOnUrl: false});
+ });
+
+ inject(function($location, $rootScope, $routeParams) {
+ $rootScope.$on('$routeChangeStart', routeChange);
+ $rootScope.$on('$routeChangeSuccess', routeChange);
+ $rootScope.$on('$routeUpdate', routeUpdate);
+
+ expect(routeChange).not.toHaveBeenCalled();
+
+ // Initial load
+ $location.path('/path/foo');
+ $rootScope.$digest();
+ expect(routeChange).toHaveBeenCalledTimes(2);
+ expect(routeUpdate).not.toHaveBeenCalled();
+ expect($routeParams).toEqual({param: 'foo'});
+
+ routeChange.calls.reset();
+
+ // Route update (no reload)
+ $location.path('/path/bar').search('foo', 'bar').hash('baz');
+ $rootScope.$digest();
+ expect(routeChange).not.toHaveBeenCalled();
+ expect(routeUpdate).toHaveBeenCalledOnce();
+ expect($routeParams).toEqual({param: 'bar', foo: 'bar'});
+ });
+ }
+ );
+
+
+ it('should update `$routeParams` even when not reloading a route', function() {
+ var routeChange = jasmine.createSpy('routeChange');
+
+ module(function($routeProvider) {
+ $routeProvider.when('/path/:param', {reloadOnUrl: false});
+ });
+
+ inject(function($location, $rootScope, $routeParams) {
+ $rootScope.$on('$routeChangeStart', routeChange);
+ $rootScope.$on('$routeChangeSuccess', routeChange);
+
+ expect(routeChange).not.toHaveBeenCalled();
+
+ // Initial load
+ $location.path('/path/foo');
+ $rootScope.$digest();
+ expect(routeChange).toHaveBeenCalledTimes(2);
+ expect($routeParams).toEqual({param: 'foo'});
+
+ routeChange.calls.reset();
+
+ // Route update (no reload)
+ $location.path('/path/bar');
+ $rootScope.$digest();
+ expect(routeChange).not.toHaveBeenCalled();
+ expect($routeParams).toEqual({param: 'bar'});
+ });
+ });
+
+
+ describe('with `$route.reload()`', function() {
+ var $location;
+ var $log;
+ var $rootScope;
+ var $route;
+ var routeChangeStart;
+ var routeChangeSuccess;
+
+ beforeEach(module(function($routeProvider) {
+ $routeProvider.when('/path/:param', {
+ template: '',
+ reloadOnUrl: false,
+ controller: function Controller($log) {
+ $log.debug('initialized');
+ }
+ });
+ }));
+
+ beforeEach(inject(function($compile, _$location_, _$log_, _$rootScope_, _$route_) {
+ $location = _$location_;
+ $log = _$log_;
+ $rootScope = _$rootScope_;
+ $route = _$route_;
+
+ routeChangeStart = jasmine.createSpy('routeChangeStart');
+ routeChangeSuccess = jasmine.createSpy('routeChangeSuccess');
+
+ $rootScope.$on('$routeChangeStart', routeChangeStart);
+ $rootScope.$on('$routeChangeSuccess', routeChangeSuccess);
+
+ element = $compile('
')($rootScope);
+ }));
+
+
+ it('should reload the current route', function() {
+ $location.path('/path/foo');
+ $rootScope.$digest();
+ expect($location.path()).toBe('/path/foo');
+ expect(routeChangeStart).toHaveBeenCalledOnce();
+ expect(routeChangeSuccess).toHaveBeenCalledOnce();
+ expect($log.debug.logs).toEqual([['initialized']]);
+
+ routeChangeStart.calls.reset();
+ routeChangeSuccess.calls.reset();
+ $log.reset();
+
+ $route.reload();
+ $rootScope.$digest();
+ expect($location.path()).toBe('/path/foo');
+ expect(routeChangeStart).toHaveBeenCalledOnce();
+ expect(routeChangeSuccess).toHaveBeenCalledOnce();
+ expect($log.debug.logs).toEqual([['initialized']]);
+
+ $log.reset();
+ });
+
+
+ it('should support preventing a route reload', function() {
+ $location.path('/path/foo');
+ $rootScope.$digest();
+ expect($location.path()).toBe('/path/foo');
+ expect(routeChangeStart).toHaveBeenCalledOnce();
+ expect(routeChangeSuccess).toHaveBeenCalledOnce();
+ expect($log.debug.logs).toEqual([['initialized']]);
+
+ routeChangeStart.calls.reset();
+ routeChangeSuccess.calls.reset();
+ $log.reset();
+
+ routeChangeStart.and.callFake(function(evt) { evt.preventDefault(); });
+
+ $route.reload();
+ $rootScope.$digest();
+ expect($location.path()).toBe('/path/foo');
+ expect(routeChangeStart).toHaveBeenCalledOnce();
+ expect(routeChangeSuccess).not.toHaveBeenCalled();
+ expect($log.debug.logs).toEqual([]);
+ });
+
+
+ it('should reload the current route even if `reloadOnUrl` is disabled',
+ inject(function($routeParams) {
+ $location.path('/path/foo');
+ $rootScope.$digest();
+ expect(routeChangeStart).toHaveBeenCalledOnce();
+ expect(routeChangeSuccess).toHaveBeenCalledOnce();
+ expect($log.debug.logs).toEqual([['initialized']]);
+ expect($routeParams).toEqual({param: 'foo'});
+
+ routeChangeStart.calls.reset();
+ routeChangeSuccess.calls.reset();
+ $log.reset();
+
+ $location.path('/path/bar');
+ $rootScope.$digest();
+ expect(routeChangeStart).not.toHaveBeenCalled();
+ expect(routeChangeSuccess).not.toHaveBeenCalled();
+ expect($log.debug.logs).toEqual([]);
+ expect($routeParams).toEqual({param: 'bar'});
+
+ $route.reload();
+ $rootScope.$digest();
+ expect(routeChangeStart).toHaveBeenCalledOnce();
+ expect(routeChangeSuccess).toHaveBeenCalledOnce();
+ expect($log.debug.logs).toEqual([['initialized']]);
+ expect($routeParams).toEqual({param: 'bar'});
+
+ $log.reset();
+ })
+ );
+ });
+ });
+
+ describe('reloadOnSearch', function() {
+ it('should not have any effect if `reloadOnUrl` is false', function() {
+ var reloaded = jasmine.createSpy('route reload');
+
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {
+ reloadOnUrl: false,
+ reloadOnSearch: true
+ });
+ });
+
+ inject(function($route, $location, $rootScope, $routeParams) {
+ $rootScope.$on('$routeChangeStart', reloaded);
+
+ $location.path('/foo');
+ $rootScope.$digest();
+ expect(reloaded).toHaveBeenCalledOnce();
+ expect($routeParams).toEqual({});
+
+ reloaded.calls.reset();
+
+ // trigger reload (via .search())
+ $location.search({foo: 'bar'});
+ $rootScope.$digest();
+ expect(reloaded).not.toHaveBeenCalled();
+ expect($routeParams).toEqual({foo: 'bar'});
+
+ // trigger reload (via .hash())
+ $location.hash('baz');
+ $rootScope.$digest();
+ expect(reloaded).not.toHaveBeenCalled();
+ expect($routeParams).toEqual({foo: 'bar'});
+ });
+ });
+
+
+ it('should reload when `reloadOnSearch` is true and `.search()`/`.hash()` changes',
+ function() {
+ var reloaded = jasmine.createSpy('route reload');
+
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {controller: angular.noop});
+ });
+
+ inject(function($route, $location, $rootScope, $routeParams) {
+ $rootScope.$on('$routeChangeStart', reloaded);
+
+ $location.path('/foo');
+ $rootScope.$digest();
+ expect(reloaded).toHaveBeenCalledOnce();
+ expect($routeParams).toEqual({});
+
+ reloaded.calls.reset();
+
+ // trigger reload (via .search())
+ $location.search({foo: 'bar'});
+ $rootScope.$digest();
+ expect(reloaded).toHaveBeenCalledOnce();
+ expect($routeParams).toEqual({foo: 'bar'});
+
+ reloaded.calls.reset();
+
+ // trigger reload (via .hash())
+ $location.hash('baz');
+ $rootScope.$digest();
+ expect(reloaded).toHaveBeenCalledOnce();
+ expect($routeParams).toEqual({foo: 'bar'});
+ });
+ }
+ );
+
+
+ it('should not reload when `reloadOnSearch` is false and `.search()`/`.hash()` changes',
+ function() {
+ var routeChange = jasmine.createSpy('route change'),
+ routeUpdate = jasmine.createSpy('route update');
+
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {controller: angular.noop, reloadOnSearch: false});
+ });
+
+ inject(function($route, $location, $rootScope) {
+ $rootScope.$on('$routeChangeStart', routeChange);
+ $rootScope.$on('$routeChangeSuccess', routeChange);
+ $rootScope.$on('$routeUpdate', routeUpdate);
+
+ expect(routeChange).not.toHaveBeenCalled();
+
+ $location.path('/foo');
+ $rootScope.$digest();
+ expect(routeChange).toHaveBeenCalledTimes(2);
+ expect(routeUpdate).not.toHaveBeenCalled();
+
+ routeChange.calls.reset();
+
+ // don't trigger reload (via .search())
+ $location.search({foo: 'bar'});
+ $rootScope.$digest();
+ expect(routeChange).not.toHaveBeenCalled();
+ expect(routeUpdate).toHaveBeenCalledOnce();
+
+ routeUpdate.calls.reset();
+
+ // don't trigger reload (via .hash())
+ $location.hash('baz');
+ $rootScope.$digest();
+ expect(routeChange).not.toHaveBeenCalled();
+ expect(routeUpdate).toHaveBeenCalled();
+ });
+ }
+ );
+
+
+ it('should reload when `reloadOnSearch` is false and url differs only in route path param',
+ function() {
+ var routeChange = jasmine.createSpy('route change');
+
+ module(function($routeProvider) {
+ $routeProvider.when('/foo/:fooId', {controller: angular.noop, reloadOnSearch: false});
+ });
+
+ inject(function($route, $location, $rootScope) {
+ $rootScope.$on('$routeChangeStart', routeChange);
+ $rootScope.$on('$routeChangeSuccess', routeChange);
+
+ expect(routeChange).not.toHaveBeenCalled();
+
+ $location.path('/foo/aaa');
+ $rootScope.$digest();
+ expect(routeChange).toHaveBeenCalledTimes(2);
+ routeChange.calls.reset();
+
+ $location.path('/foo/bbb');
+ $rootScope.$digest();
+ expect(routeChange).toHaveBeenCalledTimes(2);
+ routeChange.calls.reset();
+
+ $location.search({foo: 'bar'}).hash('baz');
+ $rootScope.$digest();
+ expect(routeChange).not.toHaveBeenCalled();
+ });
+ }
+ );
+
+
+ it('should update params when `reloadOnSearch` is false and `.search()` changes', function() {
+ var routeParamsWatcher = jasmine.createSpy('routeParamsWatcher');
+
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {controller: angular.noop});
+ $routeProvider.when('/bar/:barId', {controller: angular.noop, reloadOnSearch: false});
+ });
+
+ inject(function($route, $location, $rootScope, $routeParams) {
+ $rootScope.$watch(function() {
+ return $routeParams;
+ }, function(value) {
+ routeParamsWatcher(value);
+ }, true);
+
+ expect(routeParamsWatcher).not.toHaveBeenCalled();
+
+ $location.path('/foo');
+ $rootScope.$digest();
+ expect(routeParamsWatcher).toHaveBeenCalledWith({});
+ routeParamsWatcher.calls.reset();
+
+ // trigger reload
+ $location.search({foo: 'bar'});
+ $rootScope.$digest();
+ expect(routeParamsWatcher).toHaveBeenCalledWith({foo: 'bar'});
+ routeParamsWatcher.calls.reset();
+
+ $location.path('/bar/123').search({});
+ $rootScope.$digest();
+ expect(routeParamsWatcher).toHaveBeenCalledWith({barId: '123'});
+ routeParamsWatcher.calls.reset();
+
+ // don't trigger reload
+ $location.search({foo: 'bar'});
+ $rootScope.$digest();
+ expect(routeParamsWatcher).toHaveBeenCalledWith({barId: '123', foo: 'bar'});
+ });
+ });
+
+
+ it('should allow using a function as a template', function() {
+ var customTemplateWatcher = jasmine.createSpy('customTemplateWatcher');
+
+ function customTemplateFn(routePathParams) {
+ customTemplateWatcher(routePathParams);
+ expect(routePathParams).toEqual({id: 'id3'});
+ return '
' + routePathParams.id + '
';
+ }
+
+ module(function($routeProvider) {
+ $routeProvider.when('/bar/:id/:subid/:subsubid', {templateUrl: 'bar.html'});
+ $routeProvider.when('/foo/:id', {template: customTemplateFn});
+ });
+
+ inject(function($route, $location, $rootScope) {
+ $location.path('/foo/id3');
+ $rootScope.$digest();
+
+ expect(customTemplateWatcher).toHaveBeenCalledWith({id: 'id3'});
+ });
+ });
+
+
+ it('should allow using a function as a templateUrl', function() {
+ var customTemplateUrlWatcher = jasmine.createSpy('customTemplateUrlWatcher');
+
+ function customTemplateUrlFn(routePathParams) {
+ customTemplateUrlWatcher(routePathParams);
+ expect(routePathParams).toEqual({id: 'id3'});
+ return 'foo.html';
+ }
+
+ module(function($routeProvider) {
+ $routeProvider.when('/bar/:id/:subid/:subsubid', {templateUrl: 'bar.html'});
+ $routeProvider.when('/foo/:id', {templateUrl: customTemplateUrlFn});
+ });
+
+ inject(function($route, $location, $rootScope) {
+ $location.path('/foo/id3');
+ $rootScope.$digest();
+
+ expect(customTemplateUrlWatcher).toHaveBeenCalledWith({id: 'id3'});
+ expect($route.current.loadedTemplateUrl).toEqual('foo.html');
+ });
+ });
+
+
+ describe('with `$route.reload()`', function() {
+ var $location;
+ var $log;
+ var $rootScope;
+ var $route;
+ var routeChangeStartSpy;
+ var routeChangeSuccessSpy;
+
+ beforeEach(module(function($routeProvider) {
+ $routeProvider.when('/bar/:barId', {
+ template: '',
+ controller: controller,
+ reloadOnSearch: false
+ });
+
+ function controller($log) {
+ $log.debug('initialized');
+ }
+ }));
+ beforeEach(inject(function($compile, _$location_, _$log_, _$rootScope_, _$route_) {
+ $location = _$location_;
+ $log = _$log_;
+ $rootScope = _$rootScope_;
+ $route = _$route_;
+
+ routeChangeStartSpy = jasmine.createSpy('routeChangeStart');
+ routeChangeSuccessSpy = jasmine.createSpy('routeChangeSuccess');
+
+ $rootScope.$on('$routeChangeStart', routeChangeStartSpy);
+ $rootScope.$on('$routeChangeSuccess', routeChangeSuccessSpy);
+
+ element = $compile('
')($rootScope);
+ }));
+
+
+ it('should reload the current route', function() {
+ $location.path('/bar/123');
+ $rootScope.$digest();
+ expect($location.path()).toBe('/bar/123');
+ expect(routeChangeStartSpy).toHaveBeenCalledOnce();
+ expect(routeChangeSuccessSpy).toHaveBeenCalledOnce();
+ expect($log.debug.logs).toEqual([['initialized']]);
+
+ routeChangeStartSpy.calls.reset();
+ routeChangeSuccessSpy.calls.reset();
+ $log.reset();
+
+ $route.reload();
+ $rootScope.$digest();
+ expect($location.path()).toBe('/bar/123');
+ expect(routeChangeStartSpy).toHaveBeenCalledOnce();
+ expect(routeChangeSuccessSpy).toHaveBeenCalledOnce();
+ expect($log.debug.logs).toEqual([['initialized']]);
+
+ $log.reset();
+ });
+
+
+ it('should support preventing a route reload', function() {
+ $location.path('/bar/123');
+ $rootScope.$digest();
+ expect($location.path()).toBe('/bar/123');
+ expect(routeChangeStartSpy).toHaveBeenCalledOnce();
+ expect(routeChangeSuccessSpy).toHaveBeenCalledOnce();
+ expect($log.debug.logs).toEqual([['initialized']]);
+
+ routeChangeStartSpy.calls.reset();
+ routeChangeSuccessSpy.calls.reset();
+ $log.reset();
+
+ routeChangeStartSpy.and.callFake(function(evt) { evt.preventDefault(); });
+
+ $route.reload();
+ $rootScope.$digest();
+ expect($location.path()).toBe('/bar/123');
+ expect(routeChangeStartSpy).toHaveBeenCalledOnce();
+ expect(routeChangeSuccessSpy).not.toHaveBeenCalled();
+ expect($log.debug.logs).toEqual([]);
+ });
+
+
+ it('should reload even if reloadOnSearch is false', inject(function($routeParams) {
+ $location.path('/bar/123');
+ $rootScope.$digest();
+ expect($routeParams).toEqual({barId: '123'});
+ expect(routeChangeSuccessSpy).toHaveBeenCalledOnce();
+ expect($log.debug.logs).toEqual([['initialized']]);
+
+ routeChangeSuccessSpy.calls.reset();
+ $log.reset();
+
+ $location.search('a=b');
+ $rootScope.$digest();
+ expect($routeParams).toEqual({barId: '123', a: 'b'});
+ expect(routeChangeSuccessSpy).not.toHaveBeenCalled();
+ expect($log.debug.logs).toEqual([]);
+
+ routeChangeSuccessSpy.calls.reset();
+ $log.reset();
+
+ $location.hash('c');
+ $rootScope.$digest();
+ expect($routeParams).toEqual({barId: '123', a: 'b'});
+ expect(routeChangeSuccessSpy).not.toHaveBeenCalled();
+ expect($log.debug.logs).toEqual([]);
+
+ $route.reload();
+ $rootScope.$digest();
+ expect($routeParams).toEqual({barId: '123', a: 'b'});
+ expect(routeChangeSuccessSpy).toHaveBeenCalledOnce();
+ expect($log.debug.logs).toEqual([['initialized']]);
+
+ $log.reset();
+ }));
+ });
+ });
+
+ describe('update', function() {
+ it('should support single-parameter route updating', function() {
+ var routeChangeSpy = jasmine.createSpy('route change');
+
+ module(function($routeProvider) {
+ $routeProvider.when('/bar/:barId', {controller: angular.noop});
+ });
+
+ inject(function($route, $routeParams, $location, $rootScope) {
+ $rootScope.$on('$routeChangeSuccess', routeChangeSpy);
+
+ $location.path('/bar/1');
+ $rootScope.$digest();
+ routeChangeSpy.calls.reset();
+
+ $route.updateParams({barId: '2'});
+ $rootScope.$digest();
+
+ expect($routeParams).toEqual({barId: '2'});
+ expect(routeChangeSpy).toHaveBeenCalledOnce();
+ expect($location.path()).toEqual('/bar/2');
+ });
+ });
+
+ it('should support total multi-parameter route updating', function() {
+ var routeChangeSpy = jasmine.createSpy('route change');
+
+ module(function($routeProvider) {
+ $routeProvider.when('/bar/:barId/:fooId/:spamId/:eggId', {controller: angular.noop});
+ });
+
+ inject(function($route, $routeParams, $location, $rootScope) {
+ $rootScope.$on('$routeChangeSuccess', routeChangeSpy);
+
+ $location.path('/bar/1/2/3/4');
+ $rootScope.$digest();
+ routeChangeSpy.calls.reset();
+
+ $route.updateParams({barId: '5', fooId: '6', spamId: '7', eggId: '8'});
+ $rootScope.$digest();
+
+ expect($routeParams).toEqual({barId: '5', fooId: '6', spamId: '7', eggId: '8'});
+ expect(routeChangeSpy).toHaveBeenCalledOnce();
+ expect($location.path()).toEqual('/bar/5/6/7/8');
+ });
+ });
+
+ it('should support partial multi-parameter route updating', function() {
+ var routeChangeSpy = jasmine.createSpy('route change');
+
+ module(function($routeProvider) {
+ $routeProvider.when('/bar/:barId/:fooId/:spamId/:eggId', {controller: angular.noop});
+ });
+
+ inject(function($route, $routeParams, $location, $rootScope) {
+ $rootScope.$on('$routeChangeSuccess', routeChangeSpy);
+
+ $location.path('/bar/1/2/3/4');
+ $rootScope.$digest();
+ routeChangeSpy.calls.reset();
+
+ $route.updateParams({barId: '5', fooId: '6'});
+ $rootScope.$digest();
+
+ expect($routeParams).toEqual({barId: '5', fooId: '6', spamId: '3', eggId: '4'});
+ expect(routeChangeSpy).toHaveBeenCalledOnce();
+ expect($location.path()).toEqual('/bar/5/6/3/4');
+ });
+ });
+
+
+ it('should update query params when new properties are not in path', function() {
+ var routeChangeSpy = jasmine.createSpy('route change');
+
+ module(function($routeProvider) {
+ $routeProvider.when('/bar/:barId/:fooId/:spamId/', {controller: angular.noop});
+ });
+
+ inject(function($route, $routeParams, $location, $rootScope) {
+ $rootScope.$on('$routeChangeSuccess', routeChangeSpy);
+
+ $location.path('/bar/1/2/3');
+ $location.search({initial: 'true'});
+ $rootScope.$digest();
+ routeChangeSpy.calls.reset();
+
+ $route.updateParams({barId: '5', fooId: '6', eggId: '4'});
+ $rootScope.$digest();
+
+ expect($routeParams).toEqual({barId: '5', fooId: '6', spamId: '3', eggId: '4', initial: 'true'});
+ expect(routeChangeSpy).toHaveBeenCalledOnce();
+ expect($location.path()).toEqual('/bar/5/6/3/');
+ expect($location.search()).toEqual({eggId: '4', initial: 'true'});
+ });
+ });
+
+ it('should not update query params when an optional property was previously not in path', function() {
+ var routeChangeSpy = jasmine.createSpy('route change');
+
+ module(function($routeProvider) {
+ $routeProvider.when('/bar/:barId/:fooId/:spamId/:eggId?', {controller: angular.noop});
+ });
+
+ inject(function($route, $routeParams, $location, $rootScope) {
+ $rootScope.$on('$routeChangeSuccess', routeChangeSpy);
+
+ $location.path('/bar/1/2/3');
+ $location.search({initial: 'true'});
+ $rootScope.$digest();
+ routeChangeSpy.calls.reset();
+
+ $route.updateParams({barId: '5', fooId: '6', eggId: '4'});
+ $rootScope.$digest();
+
+ expect($routeParams).toEqual({barId: '5', fooId: '6', spamId: '3', eggId: '4', initial: 'true'});
+ expect(routeChangeSpy).toHaveBeenCalledOnce();
+ expect($location.path()).toEqual('/bar/5/6/3/4');
+ expect($location.search()).toEqual({initial: 'true'});
+ });
+ });
+
+ it('should complain if called without an existing route', inject(function($route) {
+ expect(function() { $route.updateParams(); }).toThrowMinErr('ngRoute', 'norout');
+ }));
+ });
+
+ describe('testability', function() {
+ it('should wait for $resolve promises before calling callbacks', function() {
+ var deferred;
+
+ module(function($routeProvider) {
+ $routeProvider.when('/path', {
+ resolve: {
+ a: function($q) {
+ deferred = $q.defer();
+ return deferred.promise;
+ }
+ }
+ });
+ });
+
+ inject(function($browser, $location, $rootScope, $$testability) {
+ $location.path('/path');
+ $rootScope.$digest();
+
+ var callback = jasmine.createSpy('callback');
+ $$testability.whenStable(callback);
+ expect(callback).not.toHaveBeenCalled();
+
+ deferred.resolve();
+ $browser.defer.flush();
+ expect(callback).toHaveBeenCalled();
+ });
+ });
+
+ it('should call callback after $resolve promises are rejected', function() {
+ var deferred;
+
+ module(function($routeProvider) {
+ $routeProvider.when('/path', {
+ resolve: {
+ a: function($q) {
+ deferred = $q.defer();
+ return deferred.promise;
+ }
+ }
+ });
+ });
+
+ inject(function($browser, $location, $rootScope, $$testability) {
+ $location.path('/path');
+ $rootScope.$digest();
+
+ var callback = jasmine.createSpy('callback');
+ $$testability.whenStable(callback);
+ expect(callback).not.toHaveBeenCalled();
+
+ deferred.reject();
+ $browser.defer.flush();
+ expect(callback).toHaveBeenCalled();
+ });
+ });
+
+ it('should wait for resolveRedirectTo promises before calling callbacks', function() {
+ var deferred;
+
+ module(function($routeProvider) {
+ $routeProvider.when('/path', {
+ resolveRedirectTo: function($q) {
+ deferred = $q.defer();
+ return deferred.promise;
+ }
+ });
+ });
+
+ inject(function($browser, $location, $rootScope, $$testability) {
+ $location.path('/path');
+ $rootScope.$digest();
+
+ var callback = jasmine.createSpy('callback');
+ $$testability.whenStable(callback);
+ expect(callback).not.toHaveBeenCalled();
+
+ deferred.resolve();
+ $browser.defer.flush();
+ expect(callback).toHaveBeenCalled();
+ });
+ });
+
+ it('should call callback after resolveRedirectTo promises are rejected', function() {
+ var deferred;
+
+ module(function($routeProvider) {
+ $routeProvider.when('/path', {
+ resolveRedirectTo: function($q) {
+ deferred = $q.defer();
+ return deferred.promise;
+ }
+ });
+ });
+
+ inject(function($browser, $location, $rootScope, $$testability) {
+ $location.path('/path');
+ $rootScope.$digest();
+
+ var callback = jasmine.createSpy('callback');
+ $$testability.whenStable(callback);
+ expect(callback).not.toHaveBeenCalled();
+
+ deferred.reject();
+ $browser.defer.flush();
+ expect(callback).toHaveBeenCalled();
+ });
+ });
+
+ it('should wait for all route promises before calling callbacks', function() {
+ var deferreds = {};
+
+ module(function($routeProvider) {
+ addRouteWithAsyncRedirect('/foo', '/bar');
+ addRouteWithAsyncRedirect('/bar', '/baz');
+ addRouteWithAsyncRedirect('/baz', '/qux');
+ $routeProvider.when('/qux', {
+ resolve: {
+ a: function($q) {
+ var deferred = deferreds['/qux'] = $q.defer();
+ return deferred.promise;
+ }
+ }
+ });
+
+ // Helpers
+ function addRouteWithAsyncRedirect(fromPath, toPath) {
+ $routeProvider.when(fromPath, {
+ resolveRedirectTo: function($q) {
+ var deferred = deferreds[fromPath] = $q.defer();
+ return deferred.promise.then(function() { return toPath; });
+ }
+ });
+ }
+ });
+
+ inject(function($browser, $location, $rootScope, $$testability) {
+ $location.path('/foo');
+ $rootScope.$digest();
+
+ var callback = jasmine.createSpy('callback');
+ $$testability.whenStable(callback);
+ expect(callback).not.toHaveBeenCalled();
+
+ deferreds['/foo'].resolve();
+ $browser.defer.flush();
+ expect(callback).not.toHaveBeenCalled();
+
+ deferreds['/bar'].resolve();
+ $browser.defer.flush();
+ expect(callback).not.toHaveBeenCalled();
+
+ deferreds['/baz'].resolve();
+ $browser.defer.flush();
+ expect(callback).not.toHaveBeenCalled();
+
+ deferreds['/qux'].resolve();
+ $browser.defer.flush();
+ expect(callback).toHaveBeenCalled();
+ });
+ });
+ });
+});
+
+
+})(window, window.angular);
diff --git a/snapshot/test-bundles/angular-sanitize.js b/snapshot/test-bundles/angular-sanitize.js
new file mode 100644
index 000000000..515e53f9d
--- /dev/null
+++ b/snapshot/test-bundles/angular-sanitize.js
@@ -0,0 +1,1726 @@
+/**
+ * @license AngularJS v1.8.4-local+sha.d8f77817e
+ * (c) 2010-2020 Google LLC. http://angularjs.org
+ * License: MIT
+ */
+(function(window, angular) {'use strict';
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Any commits to this file should be reviewed with security in mind. *
+ * Changes to this file can potentially create security vulnerabilities. *
+ * An approval from 2 Core members with history of modifying *
+ * this file is required. *
+ * *
+ * Does the change somehow allow for arbitrary javascript to be executed? *
+ * Or allows for someone to change the prototype of built-in objects? *
+ * Or gives undesired access to variables likes document or window? *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+var $sanitizeMinErr = angular.$$minErr('$sanitize');
+var bind;
+var extend;
+var forEach;
+var isArray;
+var isDefined;
+var lowercase;
+var noop;
+var nodeContains;
+var htmlParser;
+var htmlSanitizeWriter;
+
+/**
+ * @ngdoc module
+ * @name ngSanitize
+ * @description
+ *
+ * The `ngSanitize` module provides functionality to sanitize HTML.
+ *
+ * See {@link ngSanitize.$sanitize `$sanitize`} for usage.
+ */
+
+/**
+ * @ngdoc service
+ * @name $sanitize
+ * @kind function
+ *
+ * @description
+ * Sanitizes an html string by stripping all potentially dangerous tokens.
+ *
+ * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a trusted URI list) are
+ * then serialized back to a properly escaped HTML string. This means that no unsafe input can make
+ * it into the returned string.
+ *
+ * The trusted URIs for URL sanitization of attribute values is configured using the functions
+ * `aHrefSanitizationTrustedUrlList` and `imgSrcSanitizationTrustedUrlList` of {@link $compileProvider}.
+ *
+ * The input may also contain SVG markup if this is enabled via {@link $sanitizeProvider}.
+ *
+ * @param {string} html HTML input.
+ * @returns {string} Sanitized HTML.
+ *
+ * @example
+
+
+
+
+ Snippet:
+
+
+ | Directive |
+ How |
+ Source |
+ Rendered |
+
+
+ | ng-bind-html |
+ Automatically uses $sanitize |
+ <div ng-bind-html="snippet"> </div> |
+ |
+
+
+ | ng-bind-html |
+ Bypass $sanitize by explicitly trusting the dangerous value |
+
+ <div ng-bind-html="deliberatelyTrustDangerousSnippet()">
+</div>
+ |
+ |
+
+
+ | ng-bind |
+ Automatically escapes |
+ <div ng-bind="snippet"> </div> |
+ |
+
+
+
+
+
+ it('should sanitize the html snippet by default', function() {
+ expect(element(by.css('#bind-html-with-sanitize div')).getAttribute('innerHTML')).
+ toBe('an html\nclick here\nsnippet
');
+ });
+
+ it('should inline raw snippet if bound to a trusted value', function() {
+ expect(element(by.css('#bind-html-with-trust div')).getAttribute('innerHTML')).
+ toBe("an html\n" +
+ "click here\n" +
+ "snippet
");
+ });
+
+ it('should escape snippet without any filter', function() {
+ expect(element(by.css('#bind-default div')).getAttribute('innerHTML')).
+ toBe("<p style=\"color:blue\">an html\n" +
+ "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
+ "snippet</p>");
+ });
+
+ it('should update', function() {
+ element(by.model('snippet')).clear();
+ element(by.model('snippet')).sendKeys('new text');
+ expect(element(by.css('#bind-html-with-sanitize div')).getAttribute('innerHTML')).
+ toBe('new text');
+ expect(element(by.css('#bind-html-with-trust div')).getAttribute('innerHTML')).toBe(
+ 'new text');
+ expect(element(by.css('#bind-default div')).getAttribute('innerHTML')).toBe(
+ "new <b onclick=\"alert(1)\">text</b>");
+ });
+
+
+ */
+
+
+/**
+ * @ngdoc provider
+ * @name $sanitizeProvider
+ * @this
+ *
+ * @description
+ * Creates and configures {@link $sanitize} instance.
+ */
+function $SanitizeProvider() {
+ var hasBeenInstantiated = false;
+ var svgEnabled = false;
+
+ this.$get = ['$$sanitizeUri', function($$sanitizeUri) {
+ hasBeenInstantiated = true;
+ if (svgEnabled) {
+ extend(validElements, svgElements);
+ }
+ return function(html) {
+ var buf = [];
+ htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) {
+ return !/^unsafe:/.test($$sanitizeUri(uri, isImage));
+ }));
+ return buf.join('');
+ };
+ }];
+
+
+ /**
+ * @ngdoc method
+ * @name $sanitizeProvider#enableSvg
+ * @kind function
+ *
+ * @description
+ * Enables a subset of svg to be supported by the sanitizer.
+ *
+ *
+ *
By enabling this setting without taking other precautions, you might expose your
+ * application to click-hijacking attacks. In these attacks, sanitized svg elements could be positioned
+ * outside of the containing element and be rendered over other elements on the page (e.g. a login
+ * link). Such behavior can then result in phishing incidents.
+ *
+ *
To protect against these, explicitly setup `overflow: hidden` css rule for all potential svg
+ * tags within the sanitized content:
+ *
+ *
+ *
+ *
+ * .rootOfTheIncludedContent svg {
+ * overflow: hidden !important;
+ * }
+ *
+ *
+ *
+ * @param {boolean=} flag Enable or disable SVG support in the sanitizer.
+ * @returns {boolean|$sanitizeProvider} Returns the currently configured value if called
+ * without an argument or self for chaining otherwise.
+ */
+ this.enableSvg = function(enableSvg) {
+ if (isDefined(enableSvg)) {
+ svgEnabled = enableSvg;
+ return this;
+ } else {
+ return svgEnabled;
+ }
+ };
+
+
+ /**
+ * @ngdoc method
+ * @name $sanitizeProvider#addValidElements
+ * @kind function
+ *
+ * @description
+ * Extends the built-in lists of valid HTML/SVG elements, i.e. elements that are considered safe
+ * and are not stripped off during sanitization. You can extend the following lists of elements:
+ *
+ * - `htmlElements`: A list of elements (tag names) to extend the current list of safe HTML
+ * elements. HTML elements considered safe will not be removed during sanitization. All other
+ * elements will be stripped off.
+ *
+ * - `htmlVoidElements`: This is similar to `htmlElements`, but marks the elements as
+ * "void elements" (similar to HTML
+ * [void elements](https://rawgit.com/w3c/html/html5.1-2/single-page.html#void-elements)). These
+ * elements have no end tag and cannot have content.
+ *
+ * - `svgElements`: This is similar to `htmlElements`, but for SVG elements. This list is only
+ * taken into account if SVG is {@link ngSanitize.$sanitizeProvider#enableSvg enabled} for
+ * `$sanitize`.
+ *
+ *
+ * This method must be called during the {@link angular.Module#config config} phase. Once the
+ * `$sanitize` service has been instantiated, this method has no effect.
+ *
+ *
+ *
+ * Keep in mind that extending the built-in lists of elements may expose your app to XSS or
+ * other vulnerabilities. Be very mindful of the elements you add.
+ *
+ *
+ * @param {Array
|Object} elements - A list of valid HTML elements or an object with one or
+ * more of the following properties:
+ * - **htmlElements** - `{Array}` - A list of elements to extend the current list of
+ * HTML elements.
+ * - **htmlVoidElements** - `{Array}` - A list of elements to extend the current list of
+ * void HTML elements; i.e. elements that do not have an end tag.
+ * - **svgElements** - `{Array}` - A list of elements to extend the current list of SVG
+ * elements. The list of SVG elements is only taken into account if SVG is
+ * {@link ngSanitize.$sanitizeProvider#enableSvg enabled} for `$sanitize`.
+ *
+ * Passing an array (`[...]`) is equivalent to passing `{htmlElements: [...]}`.
+ *
+ * @return {$sanitizeProvider} Returns self for chaining.
+ */
+ this.addValidElements = function(elements) {
+ if (!hasBeenInstantiated) {
+ if (isArray(elements)) {
+ elements = {htmlElements: elements};
+ }
+
+ addElementsTo(svgElements, elements.svgElements);
+ addElementsTo(voidElements, elements.htmlVoidElements);
+ addElementsTo(validElements, elements.htmlVoidElements);
+ addElementsTo(validElements, elements.htmlElements);
+ }
+
+ return this;
+ };
+
+
+ /**
+ * @ngdoc method
+ * @name $sanitizeProvider#addValidAttrs
+ * @kind function
+ *
+ * @description
+ * Extends the built-in list of valid attributes, i.e. attributes that are considered safe and are
+ * not stripped off during sanitization.
+ *
+ * **Note**:
+ * The new attributes will not be treated as URI attributes, which means their values will not be
+ * sanitized as URIs using `$compileProvider`'s
+ * {@link ng.$compileProvider#aHrefSanitizationTrustedUrlList aHrefSanitizationTrustedUrlList} and
+ * {@link ng.$compileProvider#imgSrcSanitizationTrustedUrlList imgSrcSanitizationTrustedUrlList}.
+ *
+ *
+ * This method must be called during the {@link angular.Module#config config} phase. Once the
+ * `$sanitize` service has been instantiated, this method has no effect.
+ *
+ *
+ *
+ * Keep in mind that extending the built-in list of attributes may expose your app to XSS or
+ * other vulnerabilities. Be very mindful of the attributes you add.
+ *
+ *
+ * @param {Array} attrs - A list of valid attributes.
+ *
+ * @returns {$sanitizeProvider} Returns self for chaining.
+ */
+ this.addValidAttrs = function(attrs) {
+ if (!hasBeenInstantiated) {
+ extend(validAttrs, arrayToMap(attrs, true));
+ }
+ return this;
+ };
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ // Private stuff
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+
+ bind = angular.bind;
+ extend = angular.extend;
+ forEach = angular.forEach;
+ isArray = angular.isArray;
+ isDefined = angular.isDefined;
+ lowercase = angular.$$lowercase;
+ noop = angular.noop;
+
+ htmlParser = htmlParserImpl;
+ htmlSanitizeWriter = htmlSanitizeWriterImpl;
+
+ nodeContains = window.Node.prototype.contains || /** @this */ function(arg) {
+ // eslint-disable-next-line no-bitwise
+ return !!(this.compareDocumentPosition(arg) & 16);
+ };
+
+ // Regular Expressions for parsing tags and attributes
+ var SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
+ // Match everything outside of normal chars and " (quote character)
+ NON_ALPHANUMERIC_REGEXP = /([^#-~ |!])/g;
+
+
+ // Good source of info about elements and attributes
+ // http://dev.w3.org/html5/spec/Overview.html#semantics
+ // http://simon.html5.org/html-elements
+
+ // Safe Void Elements - HTML5
+ // http://dev.w3.org/html5/spec/Overview.html#void-elements
+ var voidElements = stringToMap('area,br,col,hr,img,wbr');
+
+ // Elements that you can, intentionally, leave open (and which close themselves)
+ // http://dev.w3.org/html5/spec/Overview.html#optional-tags
+ var optionalEndTagBlockElements = stringToMap('colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr'),
+ optionalEndTagInlineElements = stringToMap('rp,rt'),
+ optionalEndTagElements = extend({},
+ optionalEndTagInlineElements,
+ optionalEndTagBlockElements);
+
+ // Safe Block Elements - HTML5
+ var blockElements = extend({}, optionalEndTagBlockElements, stringToMap('address,article,' +
+ 'aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,' +
+ 'h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,section,table,ul'));
+
+ // Inline Elements - HTML5
+ var inlineElements = extend({}, optionalEndTagInlineElements, stringToMap('a,abbr,acronym,b,' +
+ 'bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,' +
+ 'samp,small,span,strike,strong,sub,sup,time,tt,u,var'));
+
+ // SVG Elements
+ // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements
+ // Note: the elements animate,animateColor,animateMotion,animateTransform,set are intentionally omitted.
+ // They can potentially allow for arbitrary javascript to be executed. See #11290
+ var svgElements = stringToMap('circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,' +
+ 'hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,' +
+ 'radialGradient,rect,stop,svg,switch,text,title,tspan');
+
+ // Blocked Elements (will be stripped)
+ var blockedElements = stringToMap('script,style');
+
+ var validElements = extend({},
+ voidElements,
+ blockElements,
+ inlineElements,
+ optionalEndTagElements);
+
+ //Attributes that have href and hence need to be sanitized
+ var uriAttrs = stringToMap('background,cite,href,longdesc,src,xlink:href,xml:base');
+
+ var htmlAttrs = stringToMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' +
+ 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' +
+ 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' +
+ 'scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,' +
+ 'valign,value,vspace,width');
+
+ // SVG attributes (without "id" and "name" attributes)
+ // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes
+ var svgAttrs = stringToMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' +
+ 'baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,' +
+ 'cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,' +
+ 'font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,' +
+ 'height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,' +
+ 'marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,' +
+ 'max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,' +
+ 'path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,' +
+ 'requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,' +
+ 'stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,' +
+ 'stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,' +
+ 'stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,' +
+ 'underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,' +
+ 'width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,' +
+ 'xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan', true);
+
+ var validAttrs = extend({},
+ uriAttrs,
+ svgAttrs,
+ htmlAttrs);
+
+ function stringToMap(str, lowercaseKeys) {
+ return arrayToMap(str.split(','), lowercaseKeys);
+ }
+
+ function arrayToMap(items, lowercaseKeys) {
+ var obj = {}, i;
+ for (i = 0; i < items.length; i++) {
+ obj[lowercaseKeys ? lowercase(items[i]) : items[i]] = true;
+ }
+ return obj;
+ }
+
+ function addElementsTo(elementsMap, newElements) {
+ if (newElements && newElements.length) {
+ extend(elementsMap, arrayToMap(newElements));
+ }
+ }
+
+ /**
+ * Create an inert document that contains the dirty HTML that needs sanitizing.
+ * We use the DOMParser API by default and fall back to createHTMLDocument if DOMParser is not
+ * available.
+ */
+ var getInertBodyElement /* function(html: string): HTMLBodyElement */ = (function(window, document) {
+ if (isDOMParserAvailable()) {
+ return getInertBodyElement_DOMParser;
+ }
+
+ if (!document || !document.implementation) {
+ throw $sanitizeMinErr('noinert', 'Can\'t create an inert html document');
+ }
+ var inertDocument = document.implementation.createHTMLDocument('inert');
+ var inertBodyElement = (inertDocument.documentElement || inertDocument.getDocumentElement()).querySelector('body');
+ return getInertBodyElement_InertDocument;
+
+ function isDOMParserAvailable() {
+ try {
+ return !!getInertBodyElement_DOMParser('');
+ } catch (e) {
+ return false;
+ }
+ }
+
+ function getInertBodyElement_DOMParser(html) {
+ // We add this dummy element to ensure that the rest of the content is parsed as expected
+ // e.g. leading whitespace is maintained and tags like `` do not get hoisted to the `` tag.
+ html = '' + html;
+ try {
+ var body = new window.DOMParser().parseFromString(html, 'text/html').body;
+ body.firstChild.remove();
+ return body;
+ } catch (e) {
+ return undefined;
+ }
+ }
+
+ function getInertBodyElement_InertDocument(html) {
+ inertBodyElement.innerHTML = html;
+
+ // Support: IE 9-11 only
+ // strip custom-namespaced attributes on IE<=11
+ if (document.documentMode) {
+ stripCustomNsAttrs(inertBodyElement);
+ }
+
+ return inertBodyElement;
+ }
+ })(window, window.document);
+
+ /**
+ * @example
+ * htmlParser(htmlString, {
+ * start: function(tag, attrs) {},
+ * end: function(tag) {},
+ * chars: function(text) {},
+ * comment: function(text) {}
+ * });
+ *
+ * @param {string} html string
+ * @param {object} handler
+ */
+ function htmlParserImpl(html, handler) {
+ if (html === null || html === undefined) {
+ html = '';
+ } else if (typeof html !== 'string') {
+ html = '' + html;
+ }
+
+ var inertBodyElement = getInertBodyElement(html);
+ if (!inertBodyElement) return '';
+
+ //mXSS protection
+ var mXSSAttempts = 5;
+ do {
+ if (mXSSAttempts === 0) {
+ throw $sanitizeMinErr('uinput', 'Failed to sanitize html because the input is unstable');
+ }
+ mXSSAttempts--;
+
+ // trigger mXSS if it is going to happen by reading and writing the innerHTML
+ html = inertBodyElement.innerHTML;
+ inertBodyElement = getInertBodyElement(html);
+ } while (html !== inertBodyElement.innerHTML);
+
+ var node = inertBodyElement.firstChild;
+ while (node) {
+ switch (node.nodeType) {
+ case 1: // ELEMENT_NODE
+ handler.start(node.nodeName.toLowerCase(), attrToMap(node.attributes));
+ break;
+ case 3: // TEXT NODE
+ handler.chars(node.textContent);
+ break;
+ }
+
+ var nextNode;
+ if (!(nextNode = node.firstChild)) {
+ if (node.nodeType === 1) {
+ handler.end(node.nodeName.toLowerCase());
+ }
+ nextNode = getNonDescendant('nextSibling', node);
+ if (!nextNode) {
+ while (nextNode == null) {
+ node = getNonDescendant('parentNode', node);
+ if (node === inertBodyElement) break;
+ nextNode = getNonDescendant('nextSibling', node);
+ if (node.nodeType === 1) {
+ handler.end(node.nodeName.toLowerCase());
+ }
+ }
+ }
+ }
+ node = nextNode;
+ }
+
+ while ((node = inertBodyElement.firstChild)) {
+ inertBodyElement.removeChild(node);
+ }
+ }
+
+ function attrToMap(attrs) {
+ var map = {};
+ for (var i = 0, ii = attrs.length; i < ii; i++) {
+ var attr = attrs[i];
+ map[attr.name] = attr.value;
+ }
+ return map;
+ }
+
+
+ /**
+ * Escapes all potentially dangerous characters, so that the
+ * resulting string can be safely inserted into attribute or
+ * element text.
+ * @param value
+ * @returns {string} escaped text
+ */
+ function encodeEntities(value) {
+ return value.
+ replace(/&/g, '&').
+ replace(SURROGATE_PAIR_REGEXP, function(value) {
+ var hi = value.charCodeAt(0);
+ var low = value.charCodeAt(1);
+ return '' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';';
+ }).
+ replace(NON_ALPHANUMERIC_REGEXP, function(value) {
+ return '' + value.charCodeAt(0) + ';';
+ }).
+ replace(//g, '>');
+ }
+
+ /**
+ * create an HTML/XML writer which writes to buffer
+ * @param {Array} buf use buf.join('') to get out sanitized html string
+ * @returns {object} in the form of {
+ * start: function(tag, attrs) {},
+ * end: function(tag) {},
+ * chars: function(text) {},
+ * comment: function(text) {}
+ * }
+ */
+ function htmlSanitizeWriterImpl(buf, uriValidator) {
+ var ignoreCurrentElement = false;
+ var out = bind(buf, buf.push);
+ return {
+ start: function(tag, attrs) {
+ tag = lowercase(tag);
+ if (!ignoreCurrentElement && blockedElements[tag]) {
+ ignoreCurrentElement = tag;
+ }
+ if (!ignoreCurrentElement && validElements[tag] === true) {
+ out('<');
+ out(tag);
+ forEach(attrs, function(value, key) {
+ var lkey = lowercase(key);
+ var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background');
+ if (validAttrs[lkey] === true &&
+ (uriAttrs[lkey] !== true || uriValidator(value, isImage))) {
+ out(' ');
+ out(key);
+ out('="');
+ out(encodeEntities(value));
+ out('"');
+ }
+ });
+ out('>');
+ }
+ },
+ end: function(tag) {
+ tag = lowercase(tag);
+ if (!ignoreCurrentElement && validElements[tag] === true && voidElements[tag] !== true) {
+ out('');
+ out(tag);
+ out('>');
+ }
+ // eslint-disable-next-line eqeqeq
+ if (tag == ignoreCurrentElement) {
+ ignoreCurrentElement = false;
+ }
+ },
+ chars: function(chars) {
+ if (!ignoreCurrentElement) {
+ out(encodeEntities(chars));
+ }
+ }
+ };
+ }
+
+
+ /**
+ * When IE9-11 comes across an unknown namespaced attribute e.g. 'xlink:foo' it adds 'xmlns:ns1' attribute to declare
+ * ns1 namespace and prefixes the attribute with 'ns1' (e.g. 'ns1:xlink:foo'). This is undesirable since we don't want
+ * to allow any of these custom attributes. This method strips them all.
+ *
+ * @param node Root element to process
+ */
+ function stripCustomNsAttrs(node) {
+ while (node) {
+ if (node.nodeType === window.Node.ELEMENT_NODE) {
+ var attrs = node.attributes;
+ for (var i = 0, l = attrs.length; i < l; i++) {
+ var attrNode = attrs[i];
+ var attrName = attrNode.name.toLowerCase();
+ if (attrName === 'xmlns:ns1' || attrName.lastIndexOf('ns1:', 0) === 0) {
+ node.removeAttributeNode(attrNode);
+ i--;
+ l--;
+ }
+ }
+ }
+
+ var nextNode = node.firstChild;
+ if (nextNode) {
+ stripCustomNsAttrs(nextNode);
+ }
+
+ node = getNonDescendant('nextSibling', node);
+ }
+ }
+
+ function getNonDescendant(propName, node) {
+ // An element is clobbered if its `propName` property points to one of its descendants
+ var nextNode = node[propName];
+ if (nextNode && nodeContains.call(node, nextNode)) {
+ throw $sanitizeMinErr('elclob', 'Failed to sanitize html because the element is clobbered: {0}', node.outerHTML || node.outerText);
+ }
+ return nextNode;
+ }
+}
+
+function sanitizeText(chars) {
+ var buf = [];
+ var writer = htmlSanitizeWriter(buf, noop);
+ writer.chars(chars);
+ return buf.join('');
+}
+
+
+// define ngSanitize module and register $sanitize service
+angular.module('ngSanitize', [])
+ .provider('$sanitize', $SanitizeProvider)
+ .info({ angularVersion: '1.8.4-local+sha.d8f77817e' });
+
+/**
+ * @ngdoc filter
+ * @name linky
+ * @kind function
+ *
+ * @description
+ * Finds links in text input and turns them into html links. Supports `http/https/ftp/sftp/mailto` and
+ * plain email address links.
+ *
+ * Requires the {@link ngSanitize `ngSanitize`} module to be installed.
+ *
+ * @param {string} text Input text.
+ * @param {string} [target] Window (`_blank|_self|_parent|_top`) or named frame to open links in.
+ * @param {object|function(url)} [attributes] Add custom attributes to the link element.
+ *
+ * Can be one of:
+ *
+ * - `object`: A map of attributes
+ * - `function`: Takes the url as a parameter and returns a map of attributes
+ *
+ * If the map of attributes contains a value for `target`, it overrides the value of
+ * the target parameter.
+ *
+ *
+ * @returns {string} Html-linkified and {@link $sanitize sanitized} text.
+ *
+ * @usage
+
+ *
+ * @example
+
+
+
+ Snippet:
+
+
+ | Filter |
+ Source |
+ Rendered |
+
+
+ | linky filter |
+
+ <div ng-bind-html="snippet | linky"> </div>
+ |
+
+
+ |
+
+
+ | linky target |
+
+ <div ng-bind-html="snippetWithSingleURL | linky:'_blank'"> </div>
+ |
+
+
+ |
+
+
+ | linky custom attributes |
+
+ <div ng-bind-html="snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}"> </div>
+ |
+
+
+ |
+
+
+ | no filter |
+ <div ng-bind="snippet"> </div> |
+ |
+
+
+
+
+ angular.module('linkyExample', ['ngSanitize'])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.snippet =
+ 'Pretty text with some links:\n' +
+ 'http://angularjs.org/,/n' +
+ 'mailto:us@somewhere.org,\n' +
+ 'another@somewhere.org,\n' +
+ 'and one more: ftp://127.0.0.1/.';
+ $scope.snippetWithSingleURL = 'http://angularjs.org/';
+ }]);
+
+
+ it('should linkify the snippet with urls', function() {
+ expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
+ toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' +
+ 'another@somewhere.org, and one more: ftp://127.0.0.1/.');
+ expect(element.all(by.css('#linky-filter a')).count()).toEqual(4);
+ });
+
+ it('should not linkify snippet without the linky filter', function() {
+ expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()).
+ toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' +
+ 'another@somewhere.org, and one more: ftp://127.0.0.1/.');
+ expect(element.all(by.css('#escaped-html a')).count()).toEqual(0);
+ });
+
+ it('should update', function() {
+ element(by.model('snippet')).clear();
+ element(by.model('snippet')).sendKeys('new http://link.');
+ expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
+ toBe('new http://link.');
+ expect(element.all(by.css('#linky-filter a')).count()).toEqual(1);
+ expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText())
+ .toBe('new http://link.');
+ });
+
+ it('should work with the target property', function() {
+ expect(element(by.id('linky-target')).
+ element(by.binding("snippetWithSingleURL | linky:'_blank'")).getText()).
+ toBe('http://angularjs.org/');
+ expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank');
+ });
+
+ it('should optionally add custom attributes', function() {
+ expect(element(by.id('linky-custom-attributes')).
+ element(by.binding("snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}")).getText()).
+ toBe('http://angularjs.org/');
+ expect(element(by.css('#linky-custom-attributes a')).getAttribute('rel')).toEqual('nofollow');
+ });
+
+
+ */
+angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
+ var LINKY_URL_REGEXP =
+ /((s?ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i,
+ MAILTO_REGEXP = /^mailto:/i;
+
+ var linkyMinErr = angular.$$minErr('linky');
+ var isDefined = angular.isDefined;
+ var isFunction = angular.isFunction;
+ var isObject = angular.isObject;
+ var isString = angular.isString;
+
+ return function(text, target, attributes) {
+ if (text == null || text === '') return text;
+ if (!isString(text)) throw linkyMinErr('notstring', 'Expected string but received: {0}', text);
+
+ var attributesFn =
+ isFunction(attributes) ? attributes :
+ isObject(attributes) ? function getAttributesObject() {return attributes;} :
+ function getEmptyAttributesObject() {return {};};
+
+ var match;
+ var raw = text;
+ var html = [];
+ var url;
+ var i;
+ while ((match = raw.match(LINKY_URL_REGEXP))) {
+ // We can not end in these as they are sometimes found at the end of the sentence
+ url = match[0];
+ // if we did not match ftp/http/www/mailto then assume mailto
+ if (!match[2] && !match[4]) {
+ url = (match[3] ? 'http://' : 'mailto:') + url;
+ }
+ i = match.index;
+ addText(raw.substr(0, i));
+ addLink(url, match[0].replace(MAILTO_REGEXP, ''));
+ raw = raw.substring(i + match[0].length);
+ }
+ addText(raw);
+ return $sanitize(html.join(''));
+
+ function addText(text) {
+ if (!text) {
+ return;
+ }
+ html.push(sanitizeText(text));
+ }
+
+ function addLink(url, text) {
+ var key, linkAttributes = attributesFn(url);
+ html.push('
');
+ addText(text);
+ html.push('');
+ }
+ };
+}]);
+
+describe('ngBindHtml', function() {
+ beforeEach(module('ngSanitize'));
+
+ it('should set html', inject(function($rootScope, $compile) {
+ var element = $compile('
')($rootScope);
+ $rootScope.html = '
hello
';
+ $rootScope.$digest();
+ expect(lowercase(element.html())).toEqual('
hello
');
+ }));
+
+
+ it('should reset html when value is null or undefined', inject(function($compile, $rootScope) {
+ var element = $compile('
')($rootScope);
+
+ angular.forEach([null, undefined, ''], function(val) {
+ $rootScope.html = 'some val';
+ $rootScope.$digest();
+ expect(lowercase(element.html())).toEqual('some val');
+
+ $rootScope.html = val;
+ $rootScope.$digest();
+ expect(lowercase(element.html())).toEqual('');
+ });
+ }));
+});
+
+describe('linky', function() {
+ var linky;
+
+ beforeEach(module('ngSanitize'));
+
+ beforeEach(inject(function($filter) {
+ linky = $filter('linky');
+ }));
+
+ it('should do basic filter', function() {
+ expect(linky('http://ab/%20(http://a/)%20%3Chttp://a/%3E%20http://1.2/v:~-123.%20c%20%E2%80%9Chttp://example.com%E2%80%9D%20%E2%80%98http://me.com%E2%80%99')).
+ toEqual('
http://ab/ ' +
+ '(
http://a/) ' +
+ '<
http://a/> ' +
+ '
http://1.2/v:~-123. c ' +
+ '“
http://example.com” ' +
+ '‘
http://me.com’');
+ expect(linky(undefined)).not.toBeDefined();
+ });
+
+ it('should return `undefined`/`null`/`""` values unchanged', function() {
+ expect(linky(undefined)).toBeUndefined();
+ expect(linky(null)).toBe(null);
+ expect(linky('')).toBe('');
+ });
+
+ it('should throw an error when used with a non-string value (other than `undefined`/`null`)',
+ function() {
+ expect(function() { linky(false); }).
+ toThrowMinErr('linky', 'notstring', 'Expected string but received: false');
+
+ expect(function() { linky(true); }).
+ toThrowMinErr('linky', 'notstring', 'Expected string but received: true');
+
+ expect(function() { linky(0); }).
+ toThrowMinErr('linky', 'notstring', 'Expected string but received: 0');
+
+ expect(function() { linky(42); }).
+ toThrowMinErr('linky', 'notstring', 'Expected string but received: 42');
+
+ expect(function() { linky({}); }).
+ toThrowMinErr('linky', 'notstring', 'Expected string but received: {}');
+
+ expect(function() { linky([]); }).
+ toThrowMinErr('linky', 'notstring', 'Expected string but received: []');
+
+ expect(function() { linky(noop); }).
+ toThrowMinErr('linky', 'notstring', 'Expected string but received: function noop()');
+ }
+ );
+
+ it('should be case-insensitive', function() {
+ expect(linky('WWW.example.com')).toEqual('
WWW.example.com');
+ expect(linky('WWW.EXAMPLE.COM')).toEqual('
WWW.EXAMPLE.COM');
+ expect(linky('HTTP://www.example.com')).toEqual('
HTTP://www.example.com');
+ expect(linky('HTTP://example.com')).toEqual('
HTTP://example.com');
+ expect(linky('HTTPS://www.example.com')).toEqual('
HTTPS://www.example.com');
+ expect(linky('HTTPS://example.com')).toEqual('
HTTPS://example.com');
+ expect(linky('FTP://www.example.com')).toEqual('
FTP://www.example.com');
+ expect(linky('FTP://example.com')).toEqual('
FTP://example.com');
+ expect(linky('SFTP://www.example.com')).toEqual('
SFTP://www.example.com');
+ expect(linky('SFTP://example.com')).toEqual('
SFTP://example.com');
+ });
+
+ it('should handle www.', function() {
+ expect(linky('www.example.com')).toEqual('
www.example.com');
+ });
+
+ it('should handle mailto:', function() {
+ expect(linky('mailto:me@example.com')).
+ toEqual('
me@example.com');
+ expect(linky('me@example.com')).
+ toEqual('
me@example.com');
+ expect(linky('send email to me@example.com, but')).
+ toEqual('send email to
me@example.com, but');
+ expect(linky('my email is "me@example.com"')).
+ toEqual('my email is "
me@example.com"');
+ });
+
+ it('should handle quotes in the email', function() {
+ expect(linky('foo@"bar".com')).toEqual('
foo@"bar".com');
+ });
+
+ it('should handle target:', function() {
+ expect(linky('http://example.com/', '_blank')).
+ toBeOneOf('
http://example.com',
+ '
http://example.com');
+ expect(linky('http://example.com/', 'someNamedIFrame')).
+ toBeOneOf('
http://example.com',
+ '
http://example.com');
+ });
+
+ describe('custom attributes', function() {
+
+ it('should optionally add custom attributes', function() {
+ expect(linky('http://example.com/', '_self', {rel: 'nofollow'})).
+ toBeOneOf('
http://example.com',
+ '
http://example.com');
+ });
+
+
+ it('should override target parameter with custom attributes', function() {
+ expect(linky('http://example.com/', '_self', {target: '_blank'})).
+ toBeOneOf('
http://example.com',
+ '
http://example.com');
+ });
+
+
+ it('should optionally add custom attributes from function', function() {
+ expect(linky('http://example.com/', '_self', function(url) {return {'class': 'blue'};})).
+ toBeOneOf('
http://example.com',
+ '
http://example.com',
+ '
http://example.com');
+ });
+
+
+ it('should pass url as parameter to custom attribute function', function() {
+ var linkParameters = jasmine.createSpy('linkParameters').and.returnValue({'class': 'blue'});
+ linky('http://example.com/', '_self', linkParameters);
+ expect(linkParameters).toHaveBeenCalledWith('http://example.com/');
+ });
+
+
+ it('should call the attribute function for all links in the input', function() {
+ var attributeFn = jasmine.createSpy('attributeFn').and.returnValue({});
+ linky('http://example.com and http://google.com', '_self', attributeFn);
+ expect(attributeFn.calls.allArgs()).toEqual([['http://example.com/'], ['http://google.com/']]);
+ });
+
+
+ it('should strip unsafe attributes', function() {
+ expect(linky('http://example.com/', '_self', {'class': 'blue', 'onclick': 'alert(\'Hi\')'})).
+ toBeOneOf('
http://example.com',
+ '
http://example.com',
+ '
http://example.com');
+ });
+ });
+});
+
+describe('HTML', function() {
+ var ua = window.navigator.userAgent;
+ var isChrome = /Chrome/.test(ua) && !/Edge/.test(ua);
+
+ var expectHTML;
+
+ beforeEach(module('ngSanitize'));
+ beforeEach(function() {
+ expectHTML = function(html) {
+ var sanitize;
+ inject(function($sanitize) {
+ sanitize = $sanitize;
+ });
+ return expect(sanitize(html));
+ };
+ });
+
+ describe('htmlParser', function() {
+ /* global htmlParser */
+
+ var handler, start, text, comment;
+ beforeEach(function() {
+ text = '';
+ start = null;
+ handler = {
+ start: function(tag, attrs) {
+ start = {
+ tag: tag,
+ attrs: attrs
+ };
+ // Since different browsers handle newlines differently we trim
+ // so that it is easier to write tests.
+ for (var i = 0, ii = attrs.length; i < ii; i++) {
+ var keyValue = attrs[i];
+ var key = keyValue.key;
+ var value = keyValue.value;
+ attrs[key] = value.replace(/^\s*/, '').replace(/\s*$/, '');
+ }
+ },
+ chars: function(text_) {
+ text += text_;
+ },
+ end:function(tag) {
+ expect(tag).toEqual(start.tag);
+ },
+ comment:function(comment_) {
+ comment = comment_;
+ }
+ };
+ // Trigger the $sanitizer provider to execute, which initializes the `htmlParser` function.
+ inject(function($sanitize) {});
+ });
+
+ it('should not parse comments', function() {
+ htmlParser('', handler);
+ expect(comment).not.toBeDefined();
+ });
+
+ it('should parse basic format', function() {
+ htmlParser('
text', handler);
+ expect(start).toEqual({tag:'tag', attrs:{attr:'value'}});
+ expect(text).toEqual('text');
+ });
+
+ it('should not treat "<" followed by a non-/ or non-letter as a tag', function() {
+ expectHTML('<- text1 text2 <1 text1 text2 <{', handler).
+ toBe('<- text1 text2 <1 text1 text2 <{');
+ });
+
+ it('should accept tag delimiters such as "<" inside real tags', function() {
+ // Assert that the < is part of the text node content, and not part of a tag name.
+ htmlParser('
10 < 100
', handler);
+ expect(text).toEqual(' 10 < 100 ');
+ });
+
+ it('should parse newlines in tags', function() {
+ htmlParser('
text\ntag\n>', handler);
+ expect(start).toEqual({tag:'tag', attrs:{attr:'value'}});
+ expect(text).toEqual('text');
+ });
+
+ it('should parse newlines in attributes', function() {
+ htmlParser('text', handler);
+ expect(start).toEqual({tag:'tag', attrs:{attr:'\nvalue\n'}});
+ expect(text).toEqual('text');
+ });
+
+ it('should parse namespace', function() {
+ htmlParser('text', handler);
+ expect(start).toEqual({tag:'ns:t-a-g', attrs:{'ns:a-t-t-r':'\nvalue\n'}});
+ expect(text).toEqual('text');
+ });
+
+ it('should parse empty value attribute of node', function() {
+ htmlParser('abc', handler);
+ expect(start).toEqual({tag:'test-foo', attrs:{selected:'', value:''}});
+ expect(text).toEqual('abc');
+ });
+ });
+
+ // THESE TESTS ARE EXECUTED WITH COMPILED ANGULAR
+ it('should echo html', function() {
+ expectHTML('helloworld.').
+ toBeOneOf('helloworld.',
+ 'helloworld.');
+ });
+
+ it('should remove script', function() {
+ expectHTML('ac.').toEqual('ac.');
+ });
+
+ it('should remove script that has newline characters', function() {
+ expectHTML('ac.').toEqual('ac.');
+ });
+
+ it('should remove DOCTYPE header', function() {
+ expectHTML('').toEqual('');
+ expectHTML('').toEqual('');
+ expectHTML('ac.').toEqual('ac.');
+ expectHTML('ac.').toEqual('ac.');
+ });
+
+ it('should escape non-start tags', function() {
+ expectHTML('a< SCRIPT >A< SCRIPT >evil< / scrIpt >B< / scrIpt >c.').
+ toBe('a< SCRIPT >A< SCRIPT >evil< / scrIpt >B< / scrIpt >c.');
+ });
+
+ it('should remove attrs', function() {
+ expectHTML('ab
c').toEqual('ab
c');
+ });
+
+ it('should handle large datasets', function() {
+ // Large is non-trivial to quantify, but handling ~100,000 should be sufficient for most purposes.
+ var largeNumber = 17; // 2^17 = 131,072
+ var result = 'b
';
+ // Ideally we would use repeat, but that isn't supported in IE.
+ for (var i = 0; i < largeNumber; i++) {
+ result += result;
+ }
+ expectHTML('a' + result + 'c').toEqual('a' + result + 'c');
+ });
+
+ it('should remove style', function() {
+ expectHTML('ac.').toEqual('ac.');
+ });
+
+ it('should remove style that has newline characters', function() {
+ expectHTML('ac.').toEqual('ac.');
+ });
+
+ it('should remove script and style', function() {
+ expectHTML('ac.').toEqual('ac.');
+ });
+
+ it('should remove double nested script', function() {
+ expectHTML('ailc.').toEqual('ailc.');
+ });
+
+ it('should remove unknown names', function() {
+ expectHTML('abc').toEqual('abc');
+ });
+
+ it('should remove unsafe value', function() {
+ expectHTML('').toEqual('');
+ expectHTML('
').toEqual('
');
+ });
+
+ it('should handle self closed elements', function() {
+ expectHTML('a
c').toEqual('a
c');
+ });
+
+ it('should handle namespace', function() {
+ expectHTML('abc').toEqual('abc');
+ });
+
+ it('should handle entities', function() {
+ var everything = '' +
+ '!@#$%^&*()_+-={}[]:";\'<>?,./`~ ħ
';
+ expectHTML(everything).toEqual(everything);
+ });
+
+ it('should mangle improper html', function() {
+ // This text is encoded more than a real HTML parser would, but it should render the same.
+ expectHTML('< div rel=" " alt=abc dir=\'"\' >text< /div>').
+ toBe('< div rel="" alt=abc dir=\'"\' >text< /div>');
+ });
+
+ it('should mangle improper html2', function() {
+ // A proper HTML parser would clobber this more in most cases, but it looks reasonable.
+ expectHTML('< div rel="" / >').
+ toBe('< div rel="" / >');
+ });
+
+ it('should ignore back slash as escape', function() {
+ expectHTML('
');
+ });
+
+ it('should ignore object attributes', function() {
+ expectHTML('
').
+ toEqual('');
+ });
+
+ it('should keep spaces as prefix/postfix', function() {
+ expectHTML(' a ').toEqual(' a ');
+ });
+
+ it('should allow multiline strings', function() {
+ expectHTML('\na\n').toEqual('
a
');
+ });
+
+ it('should accept tag delimiters such as "<" inside real tags (with nesting)', function() {
+ //this is an integrated version of the 'should accept tag delimiters such as "<" inside real tags' test
+ expectHTML('
');
+ });
+
+ it('should accept non-string arguments', function() {
+ expectHTML(null).toBe('');
+ expectHTML(undefined).toBe('');
+ expectHTML(42).toBe('42');
+ expectHTML({}).toBe('[object Object]');
+ expectHTML([1, 2, 3]).toBe('1,2,3');
+ expectHTML(true).toBe('true');
+ expectHTML(false).toBe('false');
+ });
+
+
+ it('should strip svg elements if not enabled via provider', function() {
+ expectHTML('
')
+ .toEqual('');
+ });
+
+ it('should prevent mXSS attacks', function() {
+ expectHTML('
');
+ });
+
+ describe('clobbered elements', function() {
+
+ it('should throw on a form with an input named "parentNode"', function() {
+ inject(function($sanitize) {
+
+ expect(function() {
+ $sanitize('
');
+ }).toThrowMinErr('$sanitize', 'elclob');
+
+ expect(function() {
+ $sanitize('
');
+ }).toThrowMinErr('$sanitize', 'elclob');
+ });
+ });
+
+ if (!/Edge\/\d{2,}/.test(window.navigator.userAgent)) {
+ // Skip test on Edge due to a browser bug.
+ it('should throw on a form with an input named "nextSibling"', function() {
+ inject(function($sanitize) {
+
+ expect(function() {
+ $sanitize('
');
+ }).toThrowMinErr('$sanitize', 'elclob');
+
+ expect(function() {
+ $sanitize('
');
+ }).toThrowMinErr('$sanitize', 'elclob');
+
+ });
+ });
+ }
+ });
+
+ // See https://github.com/cure53/DOMPurify/blob/a992d3a75031cb8bb032e5ea8399ba972bdf9a65/src/purify.js#L439-L449
+ it('should not allow JavaScript execution when creating inert document', inject(function($sanitize) {
+ $sanitize('
');
+
+ expect(window.xxx).toBe(undefined);
+ delete window.xxx;
+ }));
+
+ // See https://github.com/cure53/DOMPurify/releases/tag/0.6.7
+ it('should not allow JavaScript hidden in badly formed HTML to get through sanitization (Firefox bug)', inject(function($sanitize) {
+ var doc = $sanitize('