diff --git a/docs/app/src/errors.js b/docs/app/src/errors.js index bd7f6bbeef83..79f508ec3447 100644 --- a/docs/app/src/errors.js +++ b/docs/app/src/errors.js @@ -13,10 +13,10 @@ angular.module('errors', ['ngSanitize']) }; return function (text, target) { - var targetHtml = target ? ' target="' + target + '"' : ''; - if (!text) return text; + var targetHtml = target ? ' target="' + target + '"' : ''; + return $sanitize(text.replace(LINKY_URL_REGEXP, function (url) { if (STACK_TRACE_REGEXP.test(url)) { return url; @@ -34,6 +34,10 @@ angular.module('errors', ['ngSanitize']) .directive('errorDisplay', ['$location', 'errorLinkFilter', function ($location, errorLinkFilter) { + var encodeAngleBrackets = function (text) { + return text.replace(//g, '>'); + }; + var interpolate = function (formatString) { var formatArgs = arguments; return formatString.replace(/\{\d+\}/g, function (match) { @@ -51,12 +55,15 @@ angular.module('errors', ['ngSanitize']) link: function (scope, element, attrs) { var search = $location.search(), formatArgs = [attrs.errorDisplay], + formattedText, i; for (i = 0; angular.isDefined(search['p'+i]); i++) { formatArgs.push(search['p'+i]); } - element.html(errorLinkFilter(interpolate.apply(null, formatArgs), '_blank')); + + formattedText = encodeAngleBrackets(interpolate.apply(null, formatArgs)); + element.html(errorLinkFilter(formattedText, '_blank')); } }; }]); diff --git a/docs/app/test/.jshintrc b/docs/app/test/.jshintrc new file mode 100644 index 000000000000..f9d367561198 --- /dev/null +++ b/docs/app/test/.jshintrc @@ -0,0 +1,25 @@ +{ + "extends": "../../../.jshintrc-base", + "browser": true, + "globals": { + // AngularJS + "angular": false, + + // ngMocks + "module": false, + "inject": true, + + // Jasmine + "jasmine": false, + "describe": false, + "ddescribe": false, + "xdescribe": false, + "it": false, + "iit": false, + "xit": false, + "beforeEach": false, + "afterEach": false, + "spyOn": false, + "expect": false + } +} diff --git a/docs/app/test/errorsSpec.js b/docs/app/test/errorsSpec.js new file mode 100644 index 000000000000..f392057d356b --- /dev/null +++ b/docs/app/test/errorsSpec.js @@ -0,0 +1,166 @@ +'use strict'; + +describe('errors', function() { + // Mock `ngSanitize` module + angular. + module('ngSanitize', []). + value('$sanitize', jasmine.createSpy('$sanitize').andCallFake(angular.identity)); + + beforeEach(module('errors')); + + + describe('errorDisplay', function() { + var $sanitize; + var errorLinkFilter; + + beforeEach(inject(function(_$sanitize_, _errorLinkFilter_) { + $sanitize = _$sanitize_; + errorLinkFilter = _errorLinkFilter_; + })); + + + it('should return empty input unchanged', function() { + var inputs = [undefined, null, false, 0, '']; + var remaining = inputs.length; + + inputs.forEach(function(falsyValue) { + expect(errorLinkFilter(falsyValue)).toBe(falsyValue); + remaining--; + }); + + expect(remaining).toBe(0); + }); + + + it('should recognize URLs and convert them to ``', function() { + var urls = [ + ['ftp://foo/bar?baz#qux'], + ['http://foo/bar?baz#qux'], + ['https://foo/bar?baz#qux'], + ['mailto:foo_bar@baz.qux', null, 'foo_bar@baz.qux'], + ['foo_bar@baz.qux', 'mailto:foo_bar@baz.qux', 'foo_bar@baz.qux'] + ]; + var remaining = urls.length; + + urls.forEach(function(values) { + var actualUrl = values[0]; + var expectedUrl = values[1] || actualUrl; + var expectedText = values[2] || expectedUrl; + var anchor = '' + expectedText + ''; + + var input = 'start ' + actualUrl + ' end'; + var output = 'start ' + anchor + ' end'; + + expect(errorLinkFilter(input)).toBe(output); + remaining--; + }); + + expect(remaining).toBe(0); + }); + + + it('should not recognize stack-traces as URLs', function() { + var urls = [ + 'ftp://foo/bar?baz#qux:4:2', + 'http://foo/bar?baz#qux:4:2', + 'https://foo/bar?baz#qux:4:2', + 'mailto:foo_bar@baz.qux:4:2', + 'foo_bar@baz.qux:4:2' + ]; + var remaining = urls.length; + + urls.forEach(function(url) { + var input = 'start ' + url + ' end'; + + expect(errorLinkFilter(input)).toBe(input); + remaining--; + }); + + expect(remaining).toBe(0); + }); + + + it('should should set `[target]` if specified', function() { + var url = 'https://foo/bar?baz#qux'; + var target = '_blank'; + var outputWithoutTarget = '' + url + ''; + var outputWithTarget = '' + url + ''; + + expect(errorLinkFilter(url)).toBe(outputWithoutTarget); + expect(errorLinkFilter(url, target)).toBe(outputWithTarget); + }); + + + it('should truncate the contents of the generated `` to 60 characters', function() { + var looongUrl = 'https://foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo'; + var truncatedUrl = 'https://foooooooooooooooooooooooooooooooooooooooooooooooo...'; + var output = '' + truncatedUrl + ''; + + expect(looongUrl.length).toBeGreaterThan(60); + expect(truncatedUrl.length).toBe(60); + expect(errorLinkFilter(looongUrl)).toBe(output); + }); + + + it('should pass the final string through `$sanitize`', function() { + $sanitize.reset(); + + var input = 'start https://foo/bar?baz#qux end'; + var output = errorLinkFilter(input); + + expect($sanitize.callCount).toBe(1); + expect($sanitize).toHaveBeenCalledWith(output); + }); + }); + + + describe('errorDisplay', function() { + var $compile; + var $location; + var $rootScope; + var errorLinkFilter; + + beforeEach(module(function($provide) { + $provide.decorator('errorLinkFilter', function() { + errorLinkFilter = jasmine.createSpy('errorLinkFilter'); + errorLinkFilter.andCallFake(angular.identity); + + return errorLinkFilter; + }); + })); + beforeEach(inject(function(_$compile_, _$location_, _$rootScope_) { + $compile = _$compile_; + $location = _$location_; + $rootScope = _$rootScope_; + })); + + + it('should set the element\s HTML', function() { + var elem = $compile('foo')($rootScope); + expect(elem.html()).toBe('bar'); + }); + + + it('should interpolate the contents against `$location.search()`', function() { + spyOn($location, 'search').andReturn({p0: 'foo', p1: 'bar'}); + + var elem = $compile('')($rootScope); + expect(elem.html()).toBe('foo = foo, bar = bar'); + }); + + + it('should pass the interpolated text through `errorLinkFilter`', function() { + $location.search = jasmine.createSpy('search').andReturn({p0: 'foo'}); + + var elem = $compile('')($rootScope); + expect(errorLinkFilter.callCount).toBe(1); + expect(errorLinkFilter).toHaveBeenCalledWith('foo = foo', '_blank'); + }); + + + it('should encode `<` and `>`', function() { + var elem = $compile('')($rootScope); + expect(elem.text()).toBe(''); + }); + }); +});