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