From b8ea7f6aba2e675b85826b0bee1f21ddd7b866a5 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Fri, 24 May 2013 11:00:14 -0700 Subject: [PATCH] feat(ngError): add error message compression and better error messages - add toThrowNg matcher --- angularFiles.js | 1 + src/Angular.js | 11 +++-- src/auto/injector.js | 12 ++--- src/jqLite.js | 14 +++--- src/loader.js | 2 +- src/ng/cacheFactory.js | 2 +- src/ng/compile.js | 33 +++++-------- src/ng/controller.js | 5 +- src/ng/directive/input.js | 7 +-- src/ng/directive/ngRepeat.js | 12 ++--- src/ng/directive/select.js | 8 ++-- src/ng/httpBackend.js | 2 +- src/ng/interpolate.js | 2 +- src/ng/location.js | 6 +-- src/ng/parse.js | 23 +++++---- src/ng/rootScope.js | 11 ++--- src/ngError.js | 47 ++++++++++++++++++ src/ngMock/angular-mocks.js | 19 ++++---- test/AngularSpec.js | 16 ++++--- test/BinderSpec.js | 2 +- test/auto/injectorSpec.js | 31 ++++++------ test/loaderSpec.js | 2 +- test/matchers.js | 6 ++- test/ng/animatorSpec.js | 2 +- test/ng/cacheFactorySpec.js | 2 +- test/ng/compileSpec.js | 41 +++++++++++----- test/ng/controllerSpec.js | 10 ++++ test/ng/directive/inputSpec.js | 14 +++--- test/ng/directive/ngRepeatSpec.js | 14 +++--- test/ng/directive/selectSpec.js | 4 +- test/ng/interpolateSpec.js | 4 +- test/ng/locationSpec.js | 8 ++-- test/ng/parseSpec.js | 10 ++-- test/ng/rootScopeSpec.js | 12 ++--- test/ngErrorSpec.js | 80 +++++++++++++++++++++++++++++++ 35 files changed, 315 insertions(+), 160 deletions(-) create mode 100644 src/ngError.js create mode 100644 test/ngErrorSpec.js diff --git a/angularFiles.js b/angularFiles.js index 1ffe3310b336..39ccec907d0a 100755 --- a/angularFiles.js +++ b/angularFiles.js @@ -5,6 +5,7 @@ angularFiles = { 'src/AngularPublic.js', 'src/jqLite.js', 'src/apis.js', + 'src/ngError.js', 'src/auto/injector.js', diff --git a/src/Angular.js b/src/Angular.js index aefe7ad5f5bc..22fb005e0663 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -215,7 +215,7 @@ function nextUid() { /** * Set or clear the hashkey for an object. - * @param obj object + * @param obj object * @param h the hashkey (!truthy to delete the hashkey) */ function setHashKey(obj, h) { @@ -590,7 +590,10 @@ function isLeafNode (node) { * @returns {*} The copy or updated `destination`, if `destination` was specified. */ function copy(source, destination){ - if (isWindow(source) || isScope(source)) throw Error("Can't copy Window or Scope"); + if (isWindow(source) || isScope(source)) { + throw ngError(43, "Can't copy! Making copies of Window or Scope instances is not supported."); + } + if (!destination) { destination = source; if (source) { @@ -603,7 +606,7 @@ function copy(source, destination){ } } } else { - if (source === destination) throw Error("Can't copy equivalent objects or arrays"); + if (source === destination) throw ngError(44, "Can't copy! Source and destination are identical."); if (isArray(source)) { destination.length = 0; for ( var i = 0; i < source.length; i++) { @@ -1055,7 +1058,7 @@ function bindJQuery() { */ function assertArg(arg, name, reason) { if (!arg) { - throw new Error("Argument '" + (name || '?') + "' is " + (reason || "required")); + throw ngError(45, "Argument '{0}' is {1}", (name || '?'), (reason || "required")); } return arg; } diff --git a/src/auto/injector.js b/src/auto/injector.js index 9c492dc54bd6..1bd56acdf639 100644 --- a/src/auto/injector.js +++ b/src/auto/injector.js @@ -422,7 +422,7 @@ function createInjector(modulesToLoad) { }, providerInjector = (providerCache.$injector = createInternalInjector(providerCache, function() { - throw Error("Unknown provider: " + path.join(' <- ')); + throw ngError(1, "Unknown provider: {0}", path.join(' <- ')); })), instanceCache = {}, instanceInjector = (instanceCache.$injector = @@ -455,7 +455,7 @@ function createInjector(modulesToLoad) { provider_ = providerInjector.instantiate(provider_); } if (!provider_.$get) { - throw Error('Provider ' + name + ' must define $get factory method.'); + throw ngError(2, "Provider '{0}' must define $get factory method.", name); } return providerCache[name + providerSuffix] = provider_; } @@ -536,12 +536,9 @@ function createInjector(modulesToLoad) { function createInternalInjector(cache, factory) { function getService(serviceName) { - if (typeof serviceName !== 'string') { - throw Error('Service name expected'); - } if (cache.hasOwnProperty(serviceName)) { if (cache[serviceName] === INSTANTIATING) { - throw Error('Circular dependency: ' + path.join(' <- ')); + throw ngError(4, 'Circular dependency found: {0}', path.join(' <- ')); } return cache[serviceName]; } else { @@ -563,6 +560,9 @@ function createInjector(modulesToLoad) { for(i = 0, length = $inject.length; i < length; i++) { key = $inject[i]; + if (typeof key !== 'string') { + throw ngError(3, 'Incorrect injection token! Expected service name as string, got {0}', key); + } args.push( locals && locals.hasOwnProperty(key) ? locals[key] diff --git a/src/jqLite.js b/src/jqLite.js index 218b9683378b..cf9d1fa168e8 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -153,7 +153,7 @@ function JQLite(element) { } if (!(this instanceof JQLite)) { if (isString(element) && element.charAt(0) != '<') { - throw Error('selectors not implemented'); + throw ngError(46, 'Looking up elements via selectors is not supported by jqLite! See: http://docs.angularjs.org/api/angular.element'); } return new JQLite(element); } @@ -627,22 +627,22 @@ forEach({ } } return false; - }; + }; events[type] = []; - - // Refer to jQuery's implementation of mouseenter & mouseleave + + // Refer to jQuery's implementation of mouseenter & mouseleave // Read about mouseenter and mouseleave: // http://www.quirksmode.org/js/events_mouse.html#link8 - var eventmap = { mouseleave : "mouseout", mouseenter : "mouseover"} + var eventmap = { mouseleave : "mouseout", mouseenter : "mouseover"}; + bindFn(element, eventmap[type], function(event) { var ret, target = this, related = event.relatedTarget; // For mousenter/leave call the handler if related is outside the target. // NB: No relatedTarget if the mouse left/entered the browser window if ( !related || (related !== target && !contains(target, related)) ){ handle(event, type); - } - + } }); } else { diff --git a/src/loader.js b/src/loader.js index 5b74a4f3ac95..7c96a20d3604 100644 --- a/src/loader.js +++ b/src/loader.js @@ -70,7 +70,7 @@ function setupModuleLoader(window) { } return ensure(modules, name, function() { if (!requires) { - throw Error('No module: ' + name); + throw ngError(47, "Module '{0}' is not available! You either misspelled the module name or forgot to load it.", name); } /** @type {!Array.>} */ diff --git a/src/ng/cacheFactory.js b/src/ng/cacheFactory.js index ce690ebfe2a4..1c23e06339eb 100644 --- a/src/ng/cacheFactory.js +++ b/src/ng/cacheFactory.js @@ -28,7 +28,7 @@ function $CacheFactoryProvider() { function cacheFactory(cacheId, options) { if (cacheId in caches) { - throw Error('cacheId ' + cacheId + ' taken'); + throw ngError(10, "CacheId '{0}' is already taken!", cacheId); } var size = 0, diff --git a/src/ng/compile.js b/src/ng/compile.js index 96529d3cdda9..be22482b033b 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -18,9 +18,6 @@ */ -var NON_ASSIGNABLE_MODEL_EXPRESSION = 'Non-assignable model expression: '; - - /** * @ngdoc function * @name ng.$compile @@ -155,7 +152,6 @@ function $CompileProvider($provide) { Suffix = 'Directive', COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/, CLASS_DIRECTIVE_REGEXP = /(([\d\w\-_]+)(?:\:([^;]+))?;?)/, - MULTI_ROOT_TEMPLATE_ERROR = 'Template must have exactly one root element. was: ', urlSanitizationWhitelist = /^\s*(https?|ftp|mailto|file):/; @@ -392,10 +388,6 @@ function $CompileProvider($provide) { }; } - function wrongMode(localName, mode) { - throw Error("Unsupported '" + mode + "' for '" + localName + "'."); - } - function safeAddClass($element, className) { try { $element.addClass(className); @@ -669,7 +661,7 @@ function $CompileProvider($provide) { compileNode = $template[0]; if ($template.length != 1 || compileNode.nodeType !== 1) { - throw new Error(MULTI_ROOT_TEMPLATE_ERROR + directiveValue); + throw ngError(12, "Template for directive '{0}' must have exactly one root element.", directiveName); } replaceWith(jqCollection, $compileNode, compileNode); @@ -755,7 +747,7 @@ function $CompileProvider($provide) { } value = $element[retrievalMethod]('$' + require + 'Controller'); if (!value && !optional) { - throw Error("No controller: " + require); + throw ngError(13, "Controller '{0}', required by directive '{1}', can't be found!", require, directiveName); } return value; } else if (isArray(require)) { @@ -783,8 +775,8 @@ function $CompileProvider($provide) { var parentScope = scope.$parent || scope; - forEach(newIsolateScopeDirective.scope, function(definiton, scopeName) { - var match = definiton.match(LOCAL_REGEXP) || [], + forEach(newIsolateScopeDirective.scope, function(definition, scopeName) { + var match = definition.match(LOCAL_REGEXP) || [], attrName = match[3] || scopeName, optional = (match[2] == '?'), mode = match[1], // @, =, or & @@ -815,8 +807,8 @@ function $CompileProvider($provide) { parentSet = parentGet.assign || function() { // reset the change, or we will throw this exception on every $digest lastValue = scope[scopeName] = parentGet(parentScope); - throw Error(NON_ASSIGNABLE_MODEL_EXPRESSION + attrs[attrName] + - ' (directive: ' + newIsolateScopeDirective.name + ')'); + throw ngError(14, "Expression '{0}' used with directive '{1}' is non-assignable!", + attrs[attrName], newIsolateScopeDirective.name); }; lastValue = scope[scopeName] = parentGet(parentScope); scope.$watch(function parentValueWatch() { @@ -846,8 +838,8 @@ function $CompileProvider($provide) { } default: { - throw Error('Invalid isolate scope definition for directive ' + - newIsolateScopeDirective.name + ': ' + definiton); + throw ngError(15, "Invalid isolate scope definition for directive '{0}'. Definition: {... {1}: '{2}' ...}", + newIsolateScopeDirective.name, scopeName, definition); } } }); @@ -1000,7 +992,8 @@ function $CompileProvider($provide) { compileNode = $template[0]; if ($template.length != 1 || compileNode.nodeType !== 1) { - throw new Error(MULTI_ROOT_TEMPLATE_ERROR + content); + throw ngError(16, "Template for directive '{0}' must have exactly one root element. Template: {1}", + origAsyncDirective.name, templateUrl); } tempTemplateAttrs = {$attr: {}}; @@ -1037,7 +1030,7 @@ function $CompileProvider($provide) { linkQueue = null; }). error(function(response, code, headers, config) { - throw Error('Failed to load template: ' + config.url); + throw ngError(17, 'Failed to load template: {0}', config.url); }); return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, controller) { @@ -1065,8 +1058,8 @@ function $CompileProvider($provide) { function assertNoDuplicate(what, previousDirective, directive, element) { if (previousDirective) { - throw Error('Multiple directives [' + previousDirective.name + ', ' + - directive.name + '] asking for ' + what + ' on: ' + startingTag(element)); + throw ngError(18, 'Multiple directives [{0}, {1}] asking for {2} on: {3}', + previousDirective.name, directive.name, what, startingTag(element)); } } diff --git a/src/ng/controller.js b/src/ng/controller.js index 5c19cf887265..2df0bde9519c 100644 --- a/src/ng/controller.js +++ b/src/ng/controller.js @@ -74,9 +74,8 @@ function $ControllerProvider() { instance = $injector.instantiate(expression, locals); if (identifier) { - if (typeof locals.$scope !== 'object') { - throw new Error('Can not export controller as "' + identifier + '". ' + - 'No scope object provided!'); + if (!(locals && typeof locals.$scope == 'object')) { + throw ngError(47, "Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`.", constructor || expression.name, identifier); } locals.$scope[identifier] = instance; diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 543ed36733a1..610396a566d8 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -475,7 +475,8 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { var patternObj = scope.$eval(pattern); if (!patternObj || !patternObj.test) { - throw new Error('Expected ' + pattern + ' to be a RegExp but was ' + patternObj); + throw ngError(5, 'ngPattern error! Expected {0} to be a RegExp but was {1}. Element: {2}', + pattern, patternObj, startingTag(element)); } return validate(patternObj, value); }; @@ -918,8 +919,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ ngModelSet = ngModelGet.assign; if (!ngModelSet) { - throw Error(NON_ASSIGNABLE_MODEL_EXPRESSION + $attr.ngModel + - ' (' + startingTag($element) + ')'); + throw ngError(6, "ngModel error! Expression '{0}' is non-assignable. Element: {1}", $attr.ngModel, + startingTag($element)); } /** diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index 330f6abb18cc..34d32f59adb2 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -157,8 +157,8 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) { hashFnLocals = {$id: hashKey}; if (!match) { - throw Error("Expected ngRepeat in form of '_item_ in _collection_[ track by _id_]' but got '" + - expression + "'."); + throw ngError(7, "ngRepeat error! Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.", + expression); } lhs = match[1]; @@ -182,8 +182,8 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) { match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/); if (!match) { - throw Error("'item' in 'item in collection' should be identifier or (key, value) but got '" + - lhs + "'."); + throw ngError(8, "ngRepeat error! '_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.", + lhs); } valueIdentifier = match[3] || match[1]; keyIdentifier = match[2]; @@ -244,8 +244,8 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) { if (block && block.element) lastBlockMap[block.id] = block; }); // This is a duplicate and we need to throw an error - throw new Error('Duplicates in a repeater are not allowed. Repeater: ' + expression + - ' key: ' + trackById); + throw ngError(50, "ngRepeat error! Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}", + expression, trackById); } else { // new never before seen block nextBlockOrder[index] = { id: trackById }; diff --git a/src/ng/directive/select.js b/src/ng/directive/select.js index 7a1cab5337ec..6dda33f5d086 100644 --- a/src/ng/directive/select.js +++ b/src/ng/directive/select.js @@ -300,9 +300,9 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { var match; if (! (match = optionsExp.match(NG_OPTIONS_REGEXP))) { - throw Error( - "Expected ngOptions in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_ (track by _expr_)?'" + - " but got '" + optionsExp + "'."); + throw ngError(9, + "ngOptions error! Expected expression in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_' but got '{0}'. Element: {1}", + optionsExp, startingTag(selectElement)); } var displayFn = $parse(match[2] || match[1]), @@ -357,7 +357,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { for (var trackIndex = 0; trackIndex < collection.length; trackIndex++) { locals[valueName] = collection[trackIndex]; if (trackFn(scope, locals) == key) break; - } + } } else { locals[valueName] = collection[key]; } diff --git a/src/ng/httpBackend.js b/src/ng/httpBackend.js index ed8404f96cfb..5b9657057e66 100644 --- a/src/ng/httpBackend.js +++ b/src/ng/httpBackend.js @@ -2,7 +2,7 @@ var XHR = window.XMLHttpRequest || function() { try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e1) {} try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (e2) {} try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e3) {} - throw new Error("This browser does not support XMLHttpRequest."); + throw ngError(19, "This browser does not support XMLHttpRequest."); }; diff --git a/src/ng/interpolate.js b/src/ng/interpolate.js index dcf05d778f3e..42218fd383d8 100644 --- a/src/ng/interpolate.js +++ b/src/ng/interpolate.js @@ -139,7 +139,7 @@ function $InterpolateProvider() { return concat.join(''); } catch(err) { - var newErr = new Error('Error while interpolating: ' + text + '\n' + err.toString()); + var newErr = ngError(48, "$interpolate error! Can't interpolate: {0}\n{1}", text, err.toString()); $exceptionHandler(newErr); } }; diff --git a/src/ng/location.js b/src/ng/location.js index 4efa019c706f..7b011abe9b57 100644 --- a/src/ng/location.js +++ b/src/ng/location.js @@ -95,7 +95,7 @@ function LocationHtml5Url(appBase, basePrefix) { matchUrl(url, parsed); var pathUrl = beginsWith(appBaseNoFile, url); if (!isString(pathUrl)) { - throw Error('Invalid url "' + url + '", missing path prefix "' + appBaseNoFile + '".'); + throw ngError(21, '$location error! Invalid url "{0}", missing path prefix "{1}".', url, appBaseNoFile); } matchAppUrl(pathUrl, parsed); extend(this, parsed); @@ -157,11 +157,11 @@ function LocationHashbangUrl(appBase, hashPrefix) { matchUrl(url, this); var withoutBaseUrl = beginsWith(appBase, url) || beginsWith(appBaseNoFile, url); if (!isString(withoutBaseUrl)) { - throw new Error('Invalid url "' + url + '", does not start with "' + appBase + '".'); + throw ngError(22, '$location error! Invalid url "{0}", does not start with "{1}".', url, appBase); } var withoutHashUrl = withoutBaseUrl.charAt(0) == '#' ? beginsWith(hashPrefix, withoutBaseUrl) : withoutBaseUrl; if (!isString(withoutHashUrl)) { - throw new Error('Invalid url "' + url + '", missing hash prefix "' + hashPrefix + '".'); + throw ngError(49, '$location error! Invalid url "{0}", missing hash prefix "{1}".', url, hashPrefix); } matchAppUrl(withoutHashUrl, this); this.$$compose(); diff --git a/src/ng/parse.js b/src/ng/parse.js index 4616d15d267b..5af52f600d6f 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -123,11 +123,11 @@ function lex(text, csp){ function throwError(error, start, end) { end = end || index; - throw Error("Lexer Error: " + error + " at column" + - (isDefined(start) - ? "s " + start + "-" + index + " [" + text.substring(start, end) + "]" - : " " + end) + - " in expression [" + text + "]."); + var colStr = (isDefined(start) ? + "s " + start + "-" + index + " [" + text.substring(start, end) + "]" + : " " + end); + throw ngError(23, "Lexer Error: {0} at column{1} in expression [{2}].", + error, colStr, text); } function readNumber() { @@ -309,15 +309,14 @@ function parser(text, json, $filter, csp){ /////////////////////////////////// function throwError(msg, token) { - throw Error("Syntax Error: Token '" + token.text + - "' " + msg + " at column " + - (token.index + 1) + " of the expression [" + - text + "] starting at [" + text.substring(token.index) + "]."); + throw ngError(24, + "Syntax Error: Token '{0}' {1} at column {2} of the expression [{3}] starting at [{4}].", + token.text, msg, (token.index + 1), text, text.substring(token.index)); } function peekToken() { if (tokens.length === 0) - throw Error("Unexpected end of expression: " + text); + throw ngError(25, "Unexpected end of expression: {0}", text); return tokens[0]; } @@ -366,7 +365,7 @@ function parser(text, json, $filter, csp){ constant: left.constant && middle.constant && right.constant }); } - + function binaryFn(left, fn, right) { return extend(function(self, locals) { return fn(self, locals, left, right); @@ -471,7 +470,7 @@ function parser(text, json, $filter, csp){ return left; } } - + function logicalOR() { var left = logicalAND(); var token; diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 904f24d4c6d5..6648655133f1 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -161,10 +161,6 @@ function $RootScopeProvider(){ var Child, child; - if (isFunction(isolate)) { - // TODO: remove at some point - throw Error('API-CHANGE: Use $controller to instantiate controllers.'); - } if (isolate) { child = new Scope(); child.$root = this.$root; @@ -560,8 +556,9 @@ function $RootScopeProvider(){ if(dirty && !(ttl--)) { clearPhase(); - throw Error(TTL + ' $digest() iterations reached. Aborting!\n' + - 'Watchers fired in the last 5 iterations: ' + toJson(watchLog)); + throw ngError(27, + '{0} $digest() iterations reached. Aborting!\nWatchers fired in the last 5 iterations: {1}', + TTL, toJson(watchLog)); } } while (dirty || asyncQueue.length); @@ -923,7 +920,7 @@ function $RootScopeProvider(){ function beginPhase(phase) { if ($rootScope.$$phase) { - throw Error($rootScope.$$phase + ' already in progress'); + throw ngError(28, '{0} already in progress', $rootScope.$$phase); } $rootScope.$$phase = phase; diff --git a/src/ngError.js b/src/ngError.js new file mode 100644 index 000000000000..d054336cc7c3 --- /dev/null +++ b/src/ngError.js @@ -0,0 +1,47 @@ +'use strict'; + +/** + * @description + * + * This object extends the error class and provides interpolation capability + * to make it easier to write and read Error messages within Angular. It can + * be called as follows: + * + * throw ngError(13, 'This {0} is {1}', foo, bar); + * + * The above will replace {0} with the value of foo, and {1} with the value of + * bar. The object is not restricted in the number of arguments it can take. + * + * If fewer arguments are specified than necessary for interpolation, the extra + * interpolation markers will be preserved in the final string. + * + * @param {...} arguments The first argument to this object is the error + * number, the second argument the message with templated points for + * Interpolation (of the for {0} for the first, {1} for the second and + * so on). The second argument onwards are interpolated into the error + * message string in order. + */ +function ngError() { + var message = '[NgErr' + arguments[0] + '] ' + arguments[1], + i = 0, + l = arguments.length - 2, + curlyRegexp, arg; + + for (; i < l; i++) { + curlyRegexp = new RegExp("\\{" + i + "\\}", "gm"); + arg = arguments[i + 2]; + + if (isFunction(arg)) { + arg = arg.toString().replace(/ \{[\s\S]*$/, ''); + } else if (!isString(arg)) { + arg = toJson(arg); + } + + message = message.replace(curlyRegexp, arg); + } + + // even if we are called as constructor we can bypass the new ngError instance and return + // an instance of a real Error that contains correct stack info + extra frame for ngError call + // TODO(i): can we rewrite the stack string to remove ngError frame? + return new Error(message); +} diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index be71e326bd3b..647c0be626ee 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -972,13 +972,12 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { if (expectation && expectation.match(method, url)) { if (!expectation.matchData(data)) - throw Error('Expected ' + expectation + ' with different data\n' + + throw new Error('Expected ' + expectation + ' with different data\n' + 'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data); if (!expectation.matchHeaders(headers)) - throw Error('Expected ' + expectation + ' with different headers\n' + - 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' + - prettyPrint(headers)); + throw new Error('Expected ' + expectation + ' with different headers\n' + + 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' + prettyPrint(headers)); expectations.shift(); @@ -1002,9 +1001,9 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { } } throw wasExpected ? - Error('No response defined !') : - Error('Unexpected request: ' + method + ' ' + url + '\n' + - (expectation ? 'Expected ' + expectation : 'No more request expected')); + new Error('No response defined !') : + new Error('Unexpected request: ' + method + ' ' + url + '\n' + + (expectation ? 'Expected ' + expectation : 'No more request expected')); } /** @@ -1299,7 +1298,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { $httpBackend.verifyNoOutstandingExpectation = function() { $rootScope.$digest(); if (expectations.length) { - throw Error('Unsatisfied requests: ' + expectations.join(', ')); + throw new Error('Unsatisfied requests: ' + expectations.join(', ')); } }; @@ -1451,7 +1450,7 @@ function MockXhr() { * * This service is just a simple decorator for {@link ng.$timeout $timeout} service * that adds a "flush" and "verifyNoPendingTasks" methods. - */ + */ angular.mock.$TimeoutDecorator = function($delegate, $browser) { @@ -1477,7 +1476,7 @@ angular.mock.$TimeoutDecorator = function($delegate, $browser) { */ $delegate.verifyNoPendingTasks = function() { if ($browser.deferredFns.length) { - throw Error('Deferred tasks to flush (' + $browser.deferredFns.length + '): ' + + throw new Error('Deferred tasks to flush (' + $browser.deferredFns.length + '): ' + formatPendingTasksAsString($browser.deferredFns)); } }; diff --git a/test/AngularSpec.js b/test/AngularSpec.js index 0e5017ad25d9..de0296239231 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -84,19 +84,21 @@ describe('angular', function() { }); it('should throw an exception if a Scope is being copied', inject(function($rootScope) { - expect(function() { copy($rootScope.$new()); }).toThrow("Can't copy Window or Scope"); + expect(function() { copy($rootScope.$new()); }). + toThrow("[NgErr43] Can't copy! Making copies of Window or Scope instances is not supported."); })); it('should throw an exception if a Window is being copied', function() { - expect(function() { copy(window); }).toThrow("Can't copy Window or Scope"); + expect(function() { copy(window); }). + toThrow("[NgErr43] Can't copy! Making copies of Window or Scope instances is not supported."); }); it('should throw an exception when source and destination are equivalent', function() { var src, dst; src = dst = {key: 'value'}; - expect(function() { copy(src, dst); }).toThrow("Can't copy equivalent objects or arrays"); + expect(function() { copy(src, dst); }).toThrow("[NgErr44] Can't copy! Source and destination are identical."); src = dst = [2, 4]; - expect(function() { copy(src, dst); }).toThrow("Can't copy equivalent objects or arrays"); + expect(function() { copy(src, dst); }).toThrow("[NgErr44] Can't copy! Source and destination are identical."); }); it('should not copy the private $$hashKey', function() { @@ -580,7 +582,7 @@ describe('angular', function() { expect(function() { angularInit(appElement, bootstrap); - }).toThrow('No module: doesntexist'); + }).toThrow("[NgErr47] Module 'doesntexist' is not available! You either misspelled the module name or forgot to load it."); }); }); @@ -724,7 +726,7 @@ describe('angular', function() { expect(function() { angular.bootstrap(element, ['doesntexist']); - }).toThrow('No module: doesntexist'); + }).toThrow("[NgErr47] Module 'doesntexist' is not available! You either misspelled the module name or forgot to load it."); expect(element.html()).toBe('{{1+2}}'); dealoc(element); @@ -783,7 +785,7 @@ describe('angular', function() { expect(function() { element.injector().get('foo'); - }).toThrow('Unknown provider: fooProvider <- foo'); + }).toThrow('[NgErr1] Unknown provider: fooProvider <- foo'); expect(element.injector().get('$http')).toBeDefined(); }); diff --git a/test/BinderSpec.js b/test/BinderSpec.js index b6a7a00d4dbe..2a1b205d1df8 100644 --- a/test/BinderSpec.js +++ b/test/BinderSpec.js @@ -175,7 +175,7 @@ describe('Binder', function() { $rootScope.error['throw'] = function() {throw 'MyError';}; errorLogs.length = 0; $rootScope.$apply(); - expect(errorLogs.shift().message).toBe('Error while interpolating: {{error.throw()}}\nMyError'); + expect(errorLogs.shift().message).toBe("[NgErr48] $interpolate error! Can't interpolate: {{error.throw()}}\nMyError"); $rootScope.error['throw'] = function() {return 'ok';}; $rootScope.$apply(); diff --git a/test/auto/injectorSpec.js b/test/auto/injectorSpec.js index 8fd03be48cbb..b59a344f0466 100644 --- a/test/auto/injectorSpec.js +++ b/test/auto/injectorSpec.js @@ -70,7 +70,7 @@ describe('injector', function() { it('should provide useful message if no provider', function() { expect(function() { injector.get('idontexist'); - }).toThrow("Unknown provider: idontexistProvider <- idontexist"); + }).toThrow("[NgErr1] Unknown provider: idontexistProvider <- idontexist"); }); @@ -79,7 +79,7 @@ describe('injector', function() { providers('b', function(a) {return 2;}); expect(function() { injector.get('b'); - }).toThrow("Unknown provider: idontexistProvider <- idontexist <- a <- b"); + }).toThrow("[NgErr1] Unknown provider: idontexistProvider <- idontexist <- a <- b"); }); @@ -127,10 +127,10 @@ describe('injector', function() { it('should fail with errors if not function or array', function() { expect(function() { injector.invoke({}); - }).toThrow("Argument 'fn' is not a function, got Object"); + }).toThrow("[NgErr45] Argument 'fn' is not a function, got Object"); expect(function() { injector.invoke(['a', 123], {}); - }).toThrow("Argument 'fn' is not a function, got number"); + }).toThrow("[NgErr45] Argument 'fn' is not a function, got number"); }); }); @@ -268,7 +268,8 @@ describe('injector', function() { it('should error on invalid module name', function() { expect(function() { createInjector(['IDontExist'], {}); - }).toThrow("No module: IDontExist"); + }).toThrow("[NgErr47] Module 'IDontExist' is not available! You either misspelled the module name or forgot to load it."); + }); @@ -551,7 +552,7 @@ describe('injector', function() { createInjector([ {} ], {}); - }).toThrow("Argument 'module' is not a function, got Object"); + }).toThrow("[NgErr45] Argument 'module' is not a function, got Object"); }); @@ -568,7 +569,7 @@ describe('injector', function() { angular.module('TestModule', [], function(xyzzy) {}); expect(function() { createInjector(['TestModule']); - }).toThrow('Unknown provider: xyzzy from TestModule'); + }).toThrow('[NgErr1] Unknown provider: xyzzy from TestModule'); }); @@ -576,7 +577,7 @@ describe('injector', function() { function myModule(xyzzy){} expect(function() { createInjector([myModule]); - }).toThrow('Unknown provider: xyzzy from ' + myModule); + }).toThrow('[NgErr1] Unknown provider: xyzzy from ' + myModule); }); @@ -584,7 +585,7 @@ describe('injector', function() { function myModule(xyzzy){} expect(function() { createInjector([['xyzzy', myModule]]); - }).toThrow('Unknown provider: xyzzy from ' + myModule); + }).toThrow('[NgErr1] Unknown provider: xyzzy from ' + myModule); }); @@ -594,7 +595,7 @@ describe('injector', function() { $provide.factory('service', function(service){}); return function(service) {} }]) - }).toThrow('Circular dependency: service'); + }).toThrow("[NgErr4] Circular dependency found: service"); }); @@ -605,7 +606,7 @@ describe('injector', function() { $provide.factory('b', function(a){}); return function(a) {} }]) - }).toThrow('Circular dependency: b <- a'); + }).toThrow('[NgErr4] Circular dependency found: b <- a'); }); }); }); @@ -695,7 +696,7 @@ describe('injector', function() { it('should throw usefull error on wrong argument type]', function() { expect(function() { $injector.invoke({}); - }).toThrow("Argument 'fn' is not a function, got Object"); + }).toThrow("[NgErr45] Argument 'fn' is not a function, got Object"); }); }); @@ -782,7 +783,7 @@ describe('injector', function() { }]); expect(function() { $injector.get('nameProvider'); - }).toThrow("Unknown provider: nameProviderProvider <- nameProvider"); + }).toThrow("[NgErr1] Unknown provider: nameProviderProvider <- nameProvider"); }); @@ -790,7 +791,7 @@ describe('injector', function() { var $injector = createInjector([]); expect(function() { $injector.get('$provide').value('a', 'b'); - }).toThrow("Unknown provider: $provideProvider <- $provide"); + }).toThrow("[NgErr1] Unknown provider: $provideProvider <- $provide"); }); @@ -800,7 +801,7 @@ describe('injector', function() { createInjector([function($provide) { $provide.value('name', 'angular') }, instanceLookupInModule]); - }).toThrow('Unknown provider: name from ' + String(instanceLookupInModule)); + }).toThrow('[NgErr1] Unknown provider: name from ' + String(instanceLookupInModule)); }); }); }); diff --git a/test/loaderSpec.js b/test/loaderSpec.js index b2341a71ae38..802d5c1dccb8 100644 --- a/test/loaderSpec.js +++ b/test/loaderSpec.js @@ -68,6 +68,6 @@ describe('module loader', function() { it('should complain of no module', function() { expect(function() { window.angular.module('dontExist'); - }).toThrow('No module: dontExist'); + }).toThrow("[NgErr47] Module 'dontExist' is not available! You either misspelled the module name or forgot to load it."); }); }); diff --git a/test/matchers.js b/test/matchers.js index 8e4be118e122..13d284e6452e 100644 --- a/test/matchers.js +++ b/test/matchers.js @@ -147,8 +147,12 @@ beforeEach(function() { return this.actual.hasClass ? this.actual.hasClass(clazz) : angular.element(this.actual).hasClass(clazz); - } + }, + toThrowNg: function(expected) { + return jasmine.Matchers.prototype.toThrow.call(this, new RegExp('\\[NgErr\\d*\\] ' + + expected.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"))); + } }); }); diff --git a/test/ng/animatorSpec.js b/test/ng/animatorSpec.js index d4d7c0ece02b..cf5667d291f4 100644 --- a/test/ng/animatorSpec.js +++ b/test/ng/animatorSpec.js @@ -719,6 +719,6 @@ describe("$animator", function() { expect(function() { var animate = $animator($rootScope, { ngAnimate: ':' }); animate.enter(); - }).toThrow("Syntax Error: Token ':' not a primary expression at column 1 of the expression [:] starting at [:]."); + }).toThrow("[NgErr24] Syntax Error: Token ':' not a primary expression at column 1 of the expression [:] starting at [:]."); })); }); diff --git a/test/ng/cacheFactorySpec.js b/test/ng/cacheFactorySpec.js index ddfadbbc3efb..4b15fd7b8ac8 100644 --- a/test/ng/cacheFactorySpec.js +++ b/test/ng/cacheFactorySpec.js @@ -15,7 +15,7 @@ describe('$cacheFactory', function() { it('should complain if the cache id is being reused', inject(function($cacheFactory) { $cacheFactory('cache1'); expect(function() { $cacheFactory('cache1'); }). - toThrow('cacheId cache1 taken'); + toThrow("[NgErr10] CacheId 'cache1' is already taken!"); })); diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 26f61357bee3..bf3d0b777c50 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -632,11 +632,11 @@ describe('$compile', function() { inject(function($compile) { expect(function() { $compile('

'); - }).toThrow('Template must have exactly one root element. was: dada'); + }).toThrow("[NgErr12] Template for directive 'noRootElem' must have exactly one root element."); expect(function() { $compile('

'); - }).toThrow('Template must have exactly one root element. was:
'); + }).toThrow("[NgErr12] Template for directive 'multiRootElem' must have exactly one root element."); // ws is ok expect(function() { @@ -985,7 +985,7 @@ describe('$compile', function() { expect(function() { $httpBackend.flush(); - }).toThrow('Failed to load template: hello.html'); + }).toThrow('[NgErr17] Failed to load template: hello.html'); expect(sortedHtml(element)).toBe('
'); } )); @@ -1005,7 +1005,7 @@ describe('$compile', function() { inject(function($compile){ expect(function() { $compile('
'); - }).toThrow('Multiple directives [sync, async] asking for template on: '+ + }).toThrow('[NgErr18] Multiple directives [sync, async] asking for template on: '+ '
'); }); }); @@ -1189,14 +1189,14 @@ describe('$compile', function() { $compile('

'); $rootScope.$digest(); expect($exceptionHandler.errors.pop().message). - toBe('Template must have exactly one root element. was: dada'); + toBe("[NgErr16] Template for directive 'template' must have exactly one root element. Template: template.html"); // multi root $templateCache.put('template.html', '
'); $compile('

'); $rootScope.$digest(); expect($exceptionHandler.errors.pop().message). - toBe('Template must have exactly one root element. was:
'); + toBe("[NgErr16] Template for directive 'template' must have exactly one root element. Template: template.html"); // ws is ok $templateCache.put('template.html', '
\n'); @@ -1456,7 +1456,7 @@ describe('$compile', function() { function($rootScope, $compile) { expect(function(){ $compile('
'); - }).toThrow('Multiple directives [iscopeA, scopeB] asking for isolated scope on: ' + + }).toThrow('[NgErr18] Multiple directives [iscopeA, scopeB] asking for isolated scope on: ' + '
'); }) ); @@ -1466,7 +1466,7 @@ describe('$compile', function() { function($rootScope, $compile) { expect(function(){ $compile('
'); - }).toThrow('Multiple directives [iscopeA, iscopeB] asking for isolated scope on: ' + + }).toThrow('[NgErr18] Multiple directives [iscopeA, iscopeB] asking for isolated scope on: ' + '
'); }) ); @@ -2074,7 +2074,7 @@ describe('$compile', function() { componentScope.ref = 'ignore me'; expect($rootScope.$apply). - toThrow("Non-assignable model expression: 'hello ' + name (directive: myComponent)"); + toThrow("[NgErr14] Expression ''hello ' + name' used with directive 'myComponent' is non-assignable!"); expect(componentScope.ref).toBe('hello world'); // reset since the exception was rethrown which prevented phase clearing $rootScope.$$phase = null; @@ -2150,7 +2150,7 @@ describe('$compile', function() { it('should throw on unknown definition', inject(function() { expect(function() { compile('
'); - }).toThrow('Invalid isolate scope definition for directive badDeclaration: xxx'); + }).toThrow("[NgErr15] Invalid isolate scope definition for directive 'badDeclaration'. Definition: {... attr: 'xxx' ...}"); })); it('should expose a $$isolateBindings property onto the scope', inject(function() { @@ -2233,6 +2233,25 @@ describe('$compile', function() { }); + it("should throw an error if required controller can't be found",function() { + module(function() { + directive('dep', function(log) { + return { + require: '^main', + link: function(scope, element, attrs, controller) { + log('dep:' + controller.name); + } + }; + }); + }); + inject(function(log, $compile, $rootScope) { + expect(function() { + $compile('
')($rootScope); + }).toThrow("[NgErr13] Controller 'main', required by directive 'dep', can't be found!"); + }); + }); + + it('should have optional controller on current element', function() { module(function() { directive('dep', function(log) { @@ -2415,7 +2434,7 @@ describe('$compile', function() { inject(function($compile) { expect(function() { $compile('
'); - }).toThrow('Multiple directives [first, second] asking for transclusion on: ' + + }).toThrow('[NgErr18] Multiple directives [first, second] asking for transclusion on: ' + '
'); }); }); diff --git a/test/ng/controllerSpec.js b/test/ng/controllerSpec.js index f0dcb407cae8..b041dec71607 100644 --- a/test/ng/controllerSpec.js +++ b/test/ng/controllerSpec.js @@ -124,5 +124,15 @@ describe('$controller', function() { expect(scope.foo).toBe(foo); expect(scope.foo.mark).toBe('foo'); }); + + + it('should throw an error if $scope is not provided', function() { + $controllerProvider.register('a.b.FooCtrl', function() { this.mark = 'foo'; }); + + expect(function() { + $controller('a.b.FooCtrl as foo'); + }).toThrow("[NgErr47] Cannot export controller 'a.b.FooCtrl' as 'foo'! No $scope object provided via `locals`."); + + }); }); }); diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index f8898074f13e..68caf1f56ad8 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -43,7 +43,7 @@ describe('NgModelController', function() { } expect(exception.message). - toMatch(/Non-assignable model expression: 1\+2 \(\)/); + toMatch(/^\[NgErr6\] ngModel error! Expression '1\+2' is non\-assignable\. Element: $/); })); @@ -457,7 +457,7 @@ describe('input', function() { expect(function() { compileInput(''); scope.$digest(); - }).toThrow("Syntax Error: Token '''' is an unexpected token at column 7 of the expression [throw ''] starting at ['']."); + }).toThrow("[NgErr24] Syntax Error: Token '''' is an unexpected token at column 7 of the expression [throw ''] starting at ['']."); }); @@ -548,11 +548,11 @@ describe('input', function() { }); - xit('should throw an error when scope pattern can\'t be found', function() { - compileInput(''); - - expect(function() { changeInputValueTo('xx'); }). - toThrow('Expected fooRegexp to be a RegExp but was undefined'); + it('should throw an error when scope pattern can\'t be found', function() { + expect(function() { + compileInput(''); + scope.$apply(); + }).toThrowNg('ngPattern error! Expected fooRegexp to be a RegExp but was undefined.'); }); }); diff --git a/test/ng/directive/ngRepeatSpec.js b/test/ng/directive/ngRepeatSpec.js index e7e9af3549cc..ac6ceb830f2e 100644 --- a/test/ng/directive/ngRepeatSpec.js +++ b/test/ng/directive/ngRepeatSpec.js @@ -269,7 +269,7 @@ describe('ngRepeat', function() { element = jqLite('
'); $compile(element)(scope); expect($exceptionHandler.errors.shift()[0].message). - toBe("Expected ngRepeat in form of '_item_ in _collection_[ track by _id_]' but got 'i dont parse'."); + toBe("[NgErr7] ngRepeat error! Expected expression in form of '_item_ in _collection_[ track by _id_]' but got 'i dont parse'."); }); @@ -277,7 +277,7 @@ describe('ngRepeat', function() { element = jqLite('
'); $compile(element)(scope); expect($exceptionHandler.errors.shift()[0].message). - toBe("'item' in 'item in collection' should be identifier or (key, value) but got 'i dont parse'."); + toBe("[NgErr8] ngRepeat error! '_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got 'i dont parse'."); }); @@ -481,7 +481,7 @@ describe('ngRepeat', function() { scope.items = [a, a, a]; scope.$digest(); expect($exceptionHandler.errors.shift().message). - toEqual('Duplicates in a repeater are not allowed. Repeater: item in items key: object:003'); + toEqual("[NgErr50] ngRepeat error! Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: item in items, Duplicate key: object:003"); // recover scope.items = [a]; @@ -501,7 +501,7 @@ describe('ngRepeat', function() { scope.items = [d, d, d]; scope.$digest(); expect($exceptionHandler.errors.shift().message). - toEqual('Duplicates in a repeater are not allowed. Repeater: item in items key: object:009'); + toEqual("[NgErr50] ngRepeat error! Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: item in items, Duplicate key: object:009"); // recover scope.items = [a]; @@ -563,7 +563,7 @@ describe('ngRepeat ngAnimate', function() { } function applyCSS(element, cssProp, cssValue) { - element.css(cssProp, cssValue); + element.css(cssProp, cssValue); element.css(vendorPrefix + cssProp, cssValue); } @@ -592,7 +592,7 @@ describe('ngRepeat ngAnimate', function() { '
' + - '{{ item }}' + + '{{ item }}' + '
' ))($rootScope); @@ -635,7 +635,7 @@ describe('ngRepeat ngAnimate', function() { '
' + - '{{ item }}' + + '{{ item }}' + '
' ))($rootScope); diff --git a/test/ng/directive/selectSpec.js b/test/ng/directive/selectSpec.js index 0cb2a81222d5..5a168f53de88 100644 --- a/test/ng/directive/selectSpec.js +++ b/test/ng/directive/selectSpec.js @@ -494,8 +494,8 @@ describe('select', function() { it('should throw when not formated "? for ? in ?"', function() { expect(function() { compile(''); - }).toThrow("Expected ngOptions in form of '_select_ (as _label_)? for (_key_,)?_value_ in" + - " _collection_ (track by _expr_)?' but got 'i dont parse'."); + }).toThrowNg("ngOptions error! Expected expression in form of '_select_ (as _label_)? for (_key_,)?_value_ in" + + " _collection_' but got 'i dont parse'."); }); diff --git a/test/ng/interpolateSpec.js b/test/ng/interpolateSpec.js index 2877f92e83e3..0af38506ae46 100644 --- a/test/ng/interpolateSpec.js +++ b/test/ng/interpolateSpec.js @@ -32,7 +32,7 @@ describe('$interpolate', function() { }; expect(function () { $interpolate('{{err()}}')($rootScope); - }).toThrow('Error while interpolating: {{err()}}\nError: oops'); + }).toThrow("[NgErr48] $interpolate error! Can't interpolate: {{err()}}\nError: oops"); })); it('should stop interpolation when encountering an exception', inject(function($interpolate, $compile, $rootScope) { @@ -43,7 +43,7 @@ describe('$interpolate', function() { $compile(dom)($rootScope); expect(function () { $rootScope.$apply(); - }).toThrow('Error while interpolating: {{err()}}\nError: oops'); + }).toThrow("[NgErr48] $interpolate error! Can't interpolate: {{err()}}\nError: oops"); expect(dom[0].innerHTML).toEqual('2'); expect(dom[1].innerHTML).toEqual('{{err()}}'); expect(dom[2].innerHTML).toEqual('{{1 + 2}}'); diff --git a/test/ng/locationSpec.js b/test/ng/locationSpec.js index c4a88fd93eb0..4aaa4d51520c 100644 --- a/test/ng/locationSpec.js +++ b/test/ng/locationSpec.js @@ -203,7 +203,7 @@ describe('$location', function() { expect(function() { url.$$parse('http://other.server.org/path#/path'); - }).toThrow('Invalid url "http://other.server.org/path#/path", missing path prefix "http://server.org/base/".'); + }).toThrow('[NgErr21] $location error! Invalid url "http://other.server.org/path#/path", missing path prefix "http://server.org/base/".'); }); @@ -212,7 +212,7 @@ describe('$location', function() { expect(function() { url.$$parse('http://server.org/path#/path'); - }).toThrow('Invalid url "http://server.org/path#/path", missing path prefix "http://server.org/base/".'); + }).toThrow('[NgErr21] $location error! Invalid url "http://server.org/path#/path", missing path prefix "http://server.org/base/".'); }); @@ -325,14 +325,14 @@ describe('$location', function() { it('should throw error when invalid server url given', function() { expect(function() { url.$$parse('http://server.org/path#/path'); - }).toThrow('Invalid url "http://server.org/path#/path", does not start with "http://www.server.org:1234/base".'); + }).toThrow('[NgErr22] $location error! Invalid url "http://server.org/path#/path", does not start with "http://www.server.org:1234/base".'); }); it('should throw error when invalid hashbang prefix given', function() { expect(function() { url.$$parse('http://www.server.org:1234/base#/path'); - }).toThrow('Invalid url "http://www.server.org:1234/base#/path", missing hash prefix "#!".'); + }).toThrow('[NgErr49] $location error! Invalid url "http://www.server.org:1234/base#/path", missing hash prefix "#!".'); }); diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index 7d3315b3cea4..c3cb0ce11f4a 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -156,11 +156,11 @@ describe('parser', function() { it('should throws exception for invalid exponent', function() { expect(function() { lex("0.5E-"); - }).toThrow(new Error('Lexer Error: Invalid exponent at column 4 in expression [0.5E-].')); + }).toThrow(new Error('[NgErr23] Lexer Error: Invalid exponent at column 4 in expression [0.5E-].')); expect(function() { lex("0.5E-A"); - }).toThrow(new Error('Lexer Error: Invalid exponent at column 4 in expression [0.5E-A].')); + }).toThrow(new Error('[NgErr23] Lexer Error: Invalid exponent at column 4 in expression [0.5E-A].')); }); it('should tokenize number starting with a dot', function() { @@ -171,7 +171,7 @@ describe('parser', function() { it('should throw error on invalid unicode', function() { expect(function() { lex("'\\u1''bla'"); - }).toThrow(new Error("Lexer Error: Invalid unicode escape [\\u1''b] at column 2 in expression ['\\u1''bla'].")); + }).toThrow(new Error("[NgErr23] Lexer Error: Invalid unicode escape [\\u1''b] at column 2 in expression ['\\u1''bla'].")); }); }); @@ -304,7 +304,7 @@ describe('parser', function() { expect(function() { scope.$eval("1|nonexistent"); - }).toThrow(new Error("Unknown provider: nonexistentFilterProvider <- nonexistentFilter")); + }).toThrow(new Error("[NgErr1] Unknown provider: nonexistentFilterProvider <- nonexistentFilter")); scope.offset = 3; expect(scope.$eval("'abcd'|substring:1:offset")).toEqual("bc"); @@ -492,7 +492,7 @@ describe('parser', function() { it('should throw exception on non-closed bracket', function() { expect(function() { scope.$eval('[].count('); - }).toThrow('Unexpected end of expression: [].count('); + }).toThrow('[NgErr25] Unexpected end of expression: [].count('); }); it('should evaluate double negation', function() { diff --git a/test/ng/rootScopeSpec.js b/test/ng/rootScopeSpec.js index cac7c16017dc..0b258c83a74f 100644 --- a/test/ng/rootScopeSpec.js +++ b/test/ng/rootScopeSpec.js @@ -215,7 +215,7 @@ describe('Scope', function() { expect(function() { $rootScope.$digest(); - }).toThrow('100 $digest() iterations reached. Aborting!\n'+ + }).toThrow('[NgErr27] 100 $digest() iterations reached. Aborting!\n'+ 'Watchers fired in the last 5 iterations: ' + '[["a; newVal: 96; oldVal: 95","b; newVal: 97; oldVal: 96"],' + '["a; newVal: 97; oldVal: 96","b; newVal: 98; oldVal: 97"],' + @@ -299,7 +299,7 @@ describe('Scope', function() { $rootScope.$watch('name', function() { expect(function() { $rootScope.$digest(); - }).toThrow('$digest already in progress'); + }).toThrow('[NgErr28] $digest already in progress'); callCount++; }); $rootScope.name = 'a'; @@ -759,7 +759,7 @@ describe('Scope', function() { $rootScope.$apply(function() { $rootScope.$apply(); }); - }).toThrow('$apply already in progress'); + }).toThrow('[NgErr28] $apply already in progress'); })); @@ -771,7 +771,7 @@ describe('Scope', function() { $rootScope.$apply(); }); }); - }).toThrow('$digest already in progress'); + }).toThrow('[NgErr28] $digest already in progress'); })); @@ -781,7 +781,7 @@ describe('Scope', function() { childScope1.$watch('x', function() { childScope1.$apply(); }); - expect(function() { childScope1.$apply(); }).toThrow('$digest already in progress'); + expect(function() { childScope1.$apply(); }).toThrow('[NgErr28] $digest already in progress'); })); @@ -798,7 +798,7 @@ describe('Scope', function() { expect(function() { childScope2.$apply(function() { childScope2.x = 'something'; - }); }).toThrow('$digest already in progress'); + }); }).toThrow('[NgErr28] $digest already in progress'); })); }); }); diff --git a/test/ngErrorSpec.js b/test/ngErrorSpec.js new file mode 100644 index 000000000000..81773322841c --- /dev/null +++ b/test/ngErrorSpec.js @@ -0,0 +1,80 @@ +'use strict'; + +describe('ngError', function() { + + var supportStackTraces = function() { + var e = new Error(); + return isDefined(e.stack); + }; + + it('should return an Error instance', function() { + var myError = ngError(); + expect(myError instanceof Error).toBe(true); + }); + + + it('should generate stack trace at the frame where ngError was called', function() { + var myError; + + function someFn() { + function nestedFn() { + myError = ngError(0, "I fail!"); + } + nestedFn(); + } + + someFn(); + + // only Chrome, Firefox have stack + if (!supportStackTraces()) return; + + expect(myError.stack).toMatch(/^[.\s\S]+nestedFn[.\s\S]+someFn.+/); + }); + + + it('should interpolate string arguments without quotes', function() { + var myError = ngError(26, 'This {0} is "{1}"', 'foo', 'bar'); + expect(myError.message).toBe('[NgErr26] This foo is "bar"'); + }); + + + 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; + + myError = ngError(26, 'arr: {0}; obj: {1}; anonFn: {2}; namedFn: {3}', + arr, obj, anonFn, namedFn); + + expect(myError.message).toContain('[NgErr26] arr: [1,2,3]; obj: {"a":123,"b":"baar"};'); + // 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 = ngError(26, 'false: {0}; zero: {1}; null: {2}; undefined: {3}; emptyStr: {4}', + false, 0, null, undefined, ''); + expect(myError.message). + toBe('[NgErr26] false: false; zero: 0; null: null; undefined: undefined; emptyStr: '); + }); + + + 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 + + var foo = 'Fooooo', + myError = ngError(26, 'This {0} is {1} on {2}', foo); + + expect(myError.message).toBe('[NgErr26] This Fooooo is {1} on {2}'); + }); + + + it('should pass through the message if no interpolation is needed', function() { + var myError = ngError(26, 'Something horrible happened!'); + expect(myError.message).toBe('[NgErr26] Something horrible happened!'); + }); +});