diff --git a/src/Angular.js b/src/Angular.js index 42942d432c01..2f5236fd8557 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -129,50 +129,6 @@ var VALIDITY_STATE_PROPERTY = 'validity'; var hasOwnProperty = Object.prototype.hasOwnProperty; -var minErrConfig = { - objectMaxDepth: 5 -}; - - /** - * @ngdoc function - * @name angular.errorHandlingConfig - * @module ng - * @kind function - * - * @description - * Configure several aspects of error handling in AngularJS if used as a setter or return the - * current configuration if used as a getter. The following options are supported: - * - * - **objectMaxDepth**: The maximum depth to which objects are traversed when stringified for error messages. - * - * Omitted or undefined options will leave the corresponding configuration values unchanged. - * - * @param {Object=} config - The configuration object. May only contain the options that need to be - * updated. Supported keys: - * - * * `objectMaxDepth` **{Number}** - The max depth for stringifying objects. Setting to a - * non-positive or non-numeric value, removes the max depth limit. - * Default: 5 - */ -function errorHandlingConfig(config) { - if (isObject(config)) { - if (isDefined(config.objectMaxDepth)) { - minErrConfig.objectMaxDepth = isValidObjectMaxDepth(config.objectMaxDepth) ? config.objectMaxDepth : NaN; - } - } else { - return minErrConfig; - } -} - -/** - * @private - * @param {Number} maxDepth - * @return {boolean} - */ -function isValidObjectMaxDepth(maxDepth) { - return isNumber(maxDepth) && maxDepth > 0; -} - /** * @private * diff --git a/src/loader.prefix b/src/loader.prefix index b5188555461e..9488eb1bef74 100644 --- a/src/loader.prefix +++ b/src/loader.prefix @@ -5,7 +5,31 @@ */ 'use strict'; (function() { + // NOTE: + // These functions are copied here from `src/Angular.js`, because they are needed inside the + // `angular-loader.js` closure and need to be available before the main `angular.js` script has + // been loaded. function isFunction(value) {return typeof value === 'function';} function isDefined(value) {return typeof value !== 'undefined';} + function isNumber(value) {return typeof value === 'number';} function isObject(value) {return value !== null && typeof value === 'object';} + function isScope(obj) {return obj && obj.$evalAsync && obj.$watch;} + function isUndefined(value) {return typeof value === 'undefined';} + function isWindow(obj) {return obj && obj.window === obj;} + function sliceArgs(args, startIndex) {return Array.prototype.slice.call(args, startIndex || 0);} + function toJsonReplacer(key, value) { + var val = value; + + if (typeof key === 'string' && key.charAt(0) === '$' && key.charAt(1) === '$') { + val = undefined; + } else if (isWindow(value)) { + val = '$WINDOW'; + } else if (value && window.document === value) { + val = '$DOCUMENT'; + } else if (isScope(value)) { + val = '$SCOPE'; + } + + return val; + } diff --git a/src/minErr.js b/src/minErr.js index c188f7725d6d..e20040319222 100644 --- a/src/minErr.js +++ b/src/minErr.js @@ -1,5 +1,55 @@ 'use strict'; +/* exported + minErrConfig, + errorHandlingConfig, + isValidObjectMaxDepth +*/ + +var minErrConfig = { + objectMaxDepth: 5 +}; + +/** + * @ngdoc function + * @name angular.errorHandlingConfig + * @module ng + * @kind function + * + * @description + * Configure several aspects of error handling in AngularJS if used as a setter or return the + * current configuration if used as a getter. The following options are supported: + * + * - **objectMaxDepth**: The maximum depth to which objects are traversed when stringified for error messages. + * + * Omitted or undefined options will leave the corresponding configuration values unchanged. + * + * @param {Object=} config - The configuration object. May only contain the options that need to be + * updated. Supported keys: + * + * * `objectMaxDepth` **{Number}** - The max depth for stringifying objects. Setting to a + * non-positive or non-numeric value, removes the max depth limit. + * Default: 5 + */ +function errorHandlingConfig(config) { + if (isObject(config)) { + if (isDefined(config.objectMaxDepth)) { + minErrConfig.objectMaxDepth = isValidObjectMaxDepth(config.objectMaxDepth) ? config.objectMaxDepth : NaN; + } + } else { + return minErrConfig; + } +} + +/** + * @private + * @param {Number} maxDepth + * @return {boolean} + */ +function isValidObjectMaxDepth(maxDepth) { + return isNumber(maxDepth) && maxDepth > 0; +} + /** * @description * diff --git a/src/stringify.js b/src/stringify.js index c33aabb48c88..22362fd9a75d 100644 --- a/src/stringify.js +++ b/src/stringify.js @@ -1,6 +1,6 @@ 'use strict'; -/* global toDebugString: true */ +/* exported toDebugString */ function serializeObject(obj, maxDepth) { var seen = []; @@ -9,7 +9,9 @@ function serializeObject(obj, maxDepth) { // and a very deep object can cause a performance issue, so we copy the object // based on this specific depth and then stringify it. if (isValidObjectMaxDepth(maxDepth)) { - obj = copy(obj, null, maxDepth); + // This file is also included in `angular-loader`, so `copy()` might not always be available in + // the closure. Therefore, it is lazily retrieved as `angular.copy()` when needed. + obj = angular.copy(obj, null, maxDepth); } return JSON.stringify(obj, function(key, val) { val = toJsonReplacer(key, val); diff --git a/test/AngularSpec.js b/test/AngularSpec.js index a44660ed51b1..70d67d7dec47 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -7,7 +7,6 @@ Float32Array, Float64Array, */ describe('angular', function() { var element, document; - var originalObjectMaxDepthInErrorMessage = minErrConfig.objectMaxDepth; beforeEach(function() { document = window.document; @@ -15,30 +14,6 @@ describe('angular', function() { afterEach(function() { dealoc(element); - minErrConfig.objectMaxDepth = originalObjectMaxDepthInErrorMessage; - }); - - describe('errorHandlingConfig', function() { - it('should get default objectMaxDepth', function() { - expect(errorHandlingConfig().objectMaxDepth).toBe(5); - }); - - it('should set objectMaxDepth', function() { - errorHandlingConfig({objectMaxDepth: 3}); - expect(errorHandlingConfig().objectMaxDepth).toBe(3); - }); - - it('should not change objectMaxDepth when undefined is supplied', function() { - errorHandlingConfig({objectMaxDepth: undefined}); - expect(errorHandlingConfig().objectMaxDepth).toBe(originalObjectMaxDepthInErrorMessage); - }); - - they('should set objectMaxDepth to NaN when $prop is supplied', - [NaN, null, true, false, -1, 0], function(maxDepth) { - errorHandlingConfig({objectMaxDepth: maxDepth}); - expect(errorHandlingConfig().objectMaxDepth).toBeNaN(); - } - ); }); describe('case', function() { diff --git a/test/minErrSpec.js b/test/minErrSpec.js index 1f0d26781256..aae001cba415 100644 --- a/test/minErrSpec.js +++ b/test/minErrSpec.js @@ -1,142 +1,168 @@ 'use strict'; -describe('minErr', function() { - - var supportStackTraces = function() { - var e = new Error(); - return isDefined(e.stack); - }; - var emptyTestError = minErr(), - testError = minErr('test'); - +describe('errors', function() { var originalObjectMaxDepthInErrorMessage = minErrConfig.objectMaxDepth; + afterEach(function() { minErrConfig.objectMaxDepth = originalObjectMaxDepthInErrorMessage; }); - it('should return an Error factory', function() { - var myError = testError('test', 'Oops'); - expect(myError instanceof Error).toBe(true); + describe('errorHandlingConfig', function() { + it('should get default objectMaxDepth', function() { + expect(errorHandlingConfig().objectMaxDepth).toBe(5); + }); + + it('should set objectMaxDepth', function() { + errorHandlingConfig({objectMaxDepth: 3}); + expect(errorHandlingConfig().objectMaxDepth).toBe(3); + }); + + it('should not change objectMaxDepth when undefined is supplied', function() { + errorHandlingConfig({objectMaxDepth: undefined}); + expect(errorHandlingConfig().objectMaxDepth).toBe(originalObjectMaxDepthInErrorMessage); + }); + + they('should set objectMaxDepth to NaN when $prop is supplied', + [NaN, null, true, false, -1, 0], function(maxDepth) { + errorHandlingConfig({objectMaxDepth: maxDepth}); + expect(errorHandlingConfig().objectMaxDepth).toBeNaN(); + } + ); }); - it('should generate stack trace at the frame where the minErr instance was called', function() { - var myError; + describe('minErr', function() { - function someFn() { - function nestedFn() { - myError = testError('fail', 'I fail!'); - } - nestedFn(); - } + var supportStackTraces = function() { + var e = new Error(); + return isDefined(e.stack); + }; + var emptyTestError = minErr(), + testError = minErr('test'); - someFn(); + it('should return an Error factory', function() { + var myError = testError('test', 'Oops'); + expect(myError instanceof Error).toBe(true); + }); - // only Chrome, Firefox have stack - if (!supportStackTraces()) return; + it('should generate stack trace at the frame where the minErr instance was called', function() { + var myError; - expect(myError.stack).toMatch(/^[.\s\S]+nestedFn[.\s\S]+someFn.+/); - }); + function someFn() { + function nestedFn() { + myError = testError('fail', 'I fail!'); + } + nestedFn(); + } - it('should interpolate string arguments without quotes', function() { - var myError = testError('1', 'This {0} is "{1}"', 'foo', 'bar'); - expect(myError.message).toMatch(/^\[test:1] This foo is "bar"/); - }); + someFn(); - it('should interpolate non-string arguments', function() { - var arr = [1, 2, 3], - obj = {a: 123, b: 'baar'}, - anonFn = function(something) { return something; }, - namedFn = function foo(something) { return something; }, - myError; + // only Chrome, Firefox have stack + if (!supportStackTraces()) return; - myError = testError('26', 'arr: {0}; obj: {1}; anonFn: {2}; namedFn: {3}', - arr, obj, anonFn, namedFn); + expect(myError.stack).toMatch(/^[.\s\S]+nestedFn[.\s\S]+someFn.+/); + }); - expect(myError.message).toContain('[test:26] arr: [1,2,3]; obj: {"a":123,"b":"baar"};'); - // Support: IE 9-11 only - // IE does not add space after "function" - expect(myError.message).toMatch(/anonFn: function\s?\(something\);/); - expect(myError.message).toContain('namedFn: function foo(something)'); - }); + it('should interpolate string arguments without quotes', function() { + var myError = testError('1', 'This {0} is "{1}"', 'foo', 'bar'); + expect(myError.message).toMatch(/^\[test:1] This foo is "bar"/); + }); - it('should not suppress falsy objects', function() { - var myError = testError('26', 'false: {0}; zero: {1}; null: {2}; undefined: {3}; emptyStr: {4}', - false, 0, null, undefined, ''); - expect(myError.message). - toMatch(/^\[test:26] false: false; zero: 0; null: null; undefined: undefined; emptyStr: /); - }); + it('should interpolate non-string arguments', function() { + var arr = [1, 2, 3], + obj = {a: 123, b: 'baar'}, + anonFn = function(something) { return something; }, + namedFn = function foo(something) { return something; }, + myError; - it('should handle arguments that are objects with cyclic references', function() { - var a = { b: { } }; - a.b.a = a; + myError = testError('26', 'arr: {0}; obj: {1}; anonFn: {2}; namedFn: {3}', + arr, obj, anonFn, namedFn); - var myError = testError('26', 'a is {0}', a); - expect(myError.message).toMatch(/a is {"b":{"a":"..."}}/); - }); + expect(myError.message).toContain('[test:26] arr: [1,2,3]; obj: {"a":123,"b":"baar"};'); + // Support: IE 9-11 only + // IE does not add space after "function" + expect(myError.message).toMatch(/anonFn: function\s?\(something\);/); + expect(myError.message).toContain('namedFn: function foo(something)'); + }); + + it('should not suppress falsy objects', function() { + var myError = testError('26', 'false: {0}; zero: {1}; null: {2}; undefined: {3}; emptyStr: {4}', + false, 0, null, undefined, ''); + expect(myError.message). + toMatch(/^\[test:26] false: false; zero: 0; null: null; undefined: undefined; emptyStr: /); + }); + + it('should handle arguments that are objects with cyclic references', function() { + var a = { b: { } }; + a.b.a = a; - it('should handle arguments that are objects with max depth', function() { - var a = {b: {c: {d: {e: {f: {g: 1}}}}}}; + var myError = testError('26', 'a is {0}', a); + expect(myError.message).toMatch(/a is {"b":{"a":"..."}}/); + }); - var myError = testError('26', 'a when objectMaxDepth is default=5 is {0}', a); - expect(myError.message).toMatch(/a when objectMaxDepth is default=5 is {"b":{"c":{"d":{"e":{"f":"..."}}}}}/); + it('should handle arguments that are objects with max depth', function() { + var a = {b: {c: {d: {e: {f: {g: 1}}}}}}; - errorHandlingConfig({objectMaxDepth: 1}); - myError = testError('26', 'a when objectMaxDepth is set to 1 is {0}', a); - expect(myError.message).toMatch(/a when objectMaxDepth is set to 1 is {"b":"..."}/); + var myError = testError('26', 'a when objectMaxDepth is default=5 is {0}', a); + expect(myError.message).toMatch(/a when objectMaxDepth is default=5 is {"b":{"c":{"d":{"e":{"f":"..."}}}}}/); - errorHandlingConfig({objectMaxDepth: 2}); - myError = testError('26', 'a when objectMaxDepth is set to 2 is {0}', a); - expect(myError.message).toMatch(/a when objectMaxDepth is set to 2 is {"b":{"c":"..."}}/); + errorHandlingConfig({objectMaxDepth: 1}); + myError = testError('26', 'a when objectMaxDepth is set to 1 is {0}', a); + expect(myError.message).toMatch(/a when objectMaxDepth is set to 1 is {"b":"..."}/); - errorHandlingConfig({objectMaxDepth: undefined}); - myError = testError('26', 'a when objectMaxDepth is set to undefined is {0}', a); - expect(myError.message).toMatch(/a when objectMaxDepth is set to undefined is {"b":{"c":"..."}}/); - }); + errorHandlingConfig({objectMaxDepth: 2}); + myError = testError('26', 'a when objectMaxDepth is set to 2 is {0}', a); + expect(myError.message).toMatch(/a when objectMaxDepth is set to 2 is {"b":{"c":"..."}}/); - they('should handle arguments that are objects and ignore max depth when objectMaxDepth = $prop', - [NaN, null, true, false, -1, 0], function(maxDepth) { - var a = {b: {c: {d: {e: {f: {g: 1}}}}}}; + errorHandlingConfig({objectMaxDepth: undefined}); + myError = testError('26', 'a when objectMaxDepth is set to undefined is {0}', a); + expect(myError.message).toMatch(/a when objectMaxDepth is set to undefined is {"b":{"c":"..."}}/); + }); - errorHandlingConfig({objectMaxDepth: maxDepth}); - var myError = testError('26', 'a is {0}', a); - expect(myError.message).toMatch(/a is {"b":{"c":{"d":{"e":{"f":{"g":1}}}}}}/); - } - ); + they('should handle arguments that are objects and ignore max depth when objectMaxDepth = $prop', + [NaN, null, true, false, -1, 0], function(maxDepth) { + var a = {b: {c: {d: {e: {f: {g: 1}}}}}}; - it('should preserve interpolation markers when fewer arguments than needed are provided', function() { - // this way we can easily see if we are passing fewer args than needed + errorHandlingConfig({objectMaxDepth: maxDepth}); + var myError = testError('26', 'a is {0}', a); + expect(myError.message).toMatch(/a is {"b":{"c":{"d":{"e":{"f":{"g":1}}}}}}/); + } + ); - var foo = 'Fooooo', - myError = testError('26', 'This {0} is {1} on {2}', foo); + it('should preserve interpolation markers when fewer arguments than needed are provided', function() { + // this way we can easily see if we are passing fewer args than needed - expect(myError.message).toMatch(/^\[test:26] This Fooooo is \{1\} on \{2\}/); - }); + var foo = 'Fooooo', + myError = testError('26', 'This {0} is {1} on {2}', foo); + expect(myError.message).toMatch(/^\[test:26] This Fooooo is \{1\} on \{2\}/); + }); - it('should pass through the message if no interpolation is needed', function() { - var myError = testError('26', 'Something horrible happened!'); - expect(myError.message).toMatch(/^\[test:26] Something horrible happened!/); - }); - it('should include a namespace in the message only if it is namespaced', function() { - var myError = emptyTestError('26', 'This is a {0}', 'Foo'); - var myNamespacedError = testError('26', 'That is a {0}', 'Bar'); - expect(myError.message).toMatch(/^\[26] This is a Foo/); - expect(myNamespacedError.message).toMatch(/^\[test:26] That is a Bar/); - }); + it('should pass through the message if no interpolation is needed', function() { + var myError = testError('26', 'Something horrible happened!'); + expect(myError.message).toMatch(/^\[test:26] Something horrible happened!/); + }); + it('should include a namespace in the message only if it is namespaced', function() { + var myError = emptyTestError('26', 'This is a {0}', 'Foo'); + var myNamespacedError = testError('26', 'That is a {0}', 'Bar'); + expect(myError.message).toMatch(/^\[26] This is a Foo/); + expect(myNamespacedError.message).toMatch(/^\[test:26] That is a Bar/); + }); - it('should accept an optional 2nd argument to construct custom errors', function() { - var normalMinErr = minErr('normal'); - expect(normalMinErr('acode', 'aproblem') instanceof TypeError).toBe(false); - var typeMinErr = minErr('type', TypeError); - expect(typeMinErr('acode', 'aproblem') instanceof TypeError).toBe(true); - }); + + it('should accept an optional 2nd argument to construct custom errors', function() { + var normalMinErr = minErr('normal'); + expect(normalMinErr('acode', 'aproblem') instanceof TypeError).toBe(false); + var typeMinErr = minErr('type', TypeError); + expect(typeMinErr('acode', 'aproblem') instanceof TypeError).toBe(true); + }); - it('should include a properly formatted error reference URL in the message', function() { - // to avoid maintaining the root URL in two locations, we only validate the parameters - expect(testError('acode', 'aproblem', 'a', 'b', 'value with space').message) - .toMatch(/^[\s\S]*\?p0=a&p1=b&p2=value%20with%20space$/); + it('should include a properly formatted error reference URL in the message', function() { + // to avoid maintaining the root URL in two locations, we only validate the parameters + expect(testError('acode', 'aproblem', 'a', 'b', 'value with space').message) + .toMatch(/^[\s\S]*\?p0=a&p1=b&p2=value%20with%20space$/); + }); }); });