diff --git a/.travis.yml b/.travis.yml index 60f9f4e..9ffe889 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,5 +3,9 @@ language: node_js node_js: - 0.10 +cache: + directories: + - node_modules + after_script: - - npm run coveralls \ No newline at end of file + - npm run coveralls diff --git a/dist/featureFlags.js b/dist/featureFlags.js index 4ef6eb7..ec59392 100644 --- a/dist/featureFlags.js +++ b/dist/featureFlags.js @@ -77,8 +77,9 @@ angular.module('feature-flags').directive('featureFlagOverrides', ['featureFlags }]); angular.module('feature-flags').service('featureFlagOverrides', ['$rootElement', function($rootElement) { - var appName = $rootElement.attr('ng-app'), - keyPrefix = 'featureFlags.' + appName + '.', + var ngAttrPrefixes = ['ng-', 'data-ng-', 'ng:', 'x-ng-', ''], + appName, + keyPrefix, localStorageAvailable = (function() { try { @@ -116,6 +117,16 @@ angular.module('feature-flags').service('featureFlagOverrides', ['$rootElement', } }; + // Need to find the appName for the angular specific prefixes + angular.forEach(ngAttrPrefixes, function(prefix) { + var name = prefix + 'app'; + if (!appName && $rootElement.attr(name)) { + appName = $rootElement.attr(name); + } + }); + + keyPrefix = 'featureFlags.' + appName + '.'; + return { isPresent: function(key) { var value = get(key); diff --git a/dist/featureFlags.min.js b/dist/featureFlags.min.js index 4815efa..b4e8ed0 100644 --- a/dist/featureFlags.min.js +++ b/dist/featureFlags.min.js @@ -1,2 +1,2 @@ /*! Angular Feature Flags v1.4.0 © 2016 Michael Taranto */ -!function(){function e(e,a,t){var r={},n=[],i=function(a){var t=e.defer();return t.resolve(a),t.promise},l=function(e){return a.isPresent(e)},f=function(e){return l(e)?"true"===a.get(e):r[e]},u=function(e){return r[e]},s=function(e){return e.forEach(function(e){r[e.key]=e.active,e.active=f(e.key)}),angular.copy(e,n),n},c=function(e){return e.then(function(e){return s(e.data||e)})},g=function(){return n},o=function(e){return angular.isArray(e)?i(s(e)):c(e)},d=function(e){e.active=!0,a.set(e.key,!0)},v=function(e){e.active=!1,a.set(e.key,!1)},y=function(e){e.active=r[e.key],a.remove(e.key)},m=function(){t&&o(t)};return m(),{set:o,get:g,enable:d,disable:v,reset:y,isOn:f,isOnByDefault:u,isOverridden:l}}angular.module("feature-flags",[]),angular.module("feature-flags").directive("featureFlag",["featureFlags","$interpolate",function(e,a){return{transclude:"element",priority:599,terminal:!0,restrict:"A",$$tlb:!0,compile:function(t,r){var n="featureFlagHide"in r;return t[0].textContent=" featureFlag: "+r.featureFlag+" is "+(n?"on":"off")+" ",function(t,r,i,l,f){var u,s;t.$watch(function(){var r=a(i.featureFlag)(t);return e.isOn(r)},function(e){var a=n?!e:e;a?(s=t.$new(),f(s,function(e){u=e,r.after(u).remove()})):(s&&(s.$destroy(),s=null),u&&(u.after(r).remove(),u=null))})}}}}]),angular.module("feature-flags").directive("featureFlagOverrides",["featureFlags",function(e){return{restrict:"A",link:function(a){a.flags=e.get(),a.isOn=e.isOn,a.isOverridden=e.isOverridden,a.enable=e.enable,a.disable=e.disable,a.reset=e.reset,a.isOnByDefault=e.isOnByDefault},template:'

Feature Flags

{{flag.name || flag.key}}
ON
OFF
DEFAULT ({{isOnByDefault(flag.key) ? \'ON\' : \'OFF\'}})
{{flag.description}}
',replace:!0}}]),angular.module("feature-flags").service("featureFlagOverrides",["$rootElement",function(e){var a=e.attr("ng-app"),t="featureFlags."+a+".",r=function(){try{return localStorage.setItem("featureFlags.availableTest","test"),localStorage.removeItem("featureFlags.availableTest"),!0}catch(e){return!1}}(),n=function(e){return t+e},i=function(e){return 0===e.indexOf(t)},l=function(e,a){r&&localStorage.setItem(n(a),e)},f=function(e){return r?localStorage.getItem(n(e)):void 0},u=function(e){r&&localStorage.removeItem(n(e))};return{isPresent:function(e){var a=f(e);return"undefined"!=typeof a&&null!==a},get:f,set:function(e,a){angular.isObject(e)?angular.forEach(e,l):l(a,e)},remove:u,reset:function(){var e;if(r)for(e in localStorage)i(e)&&localStorage.removeItem(e)}}}]),angular.module("feature-flags").provider("featureFlags",function(){var a=[];this.setInitialFlags=function(e){a=e},this.$get=["$q","featureFlagOverrides",function(t,r){return new e(t,r,a)}]})}(); \ No newline at end of file +!function(){function e(e,a,t){var r={},n=[],i=function(a){var t=e.defer();return t.resolve(a),t.promise},l=function(e){return a.isPresent(e)},f=function(e){return l(e)?"true"===a.get(e):r[e]},u=function(e){return r[e]},s=function(e){return e.forEach(function(e){r[e.key]=e.active,e.active=f(e.key)}),angular.copy(e,n),n},c=function(e){return e.then(function(e){return s(e.data||e)})},g=function(){return n},o=function(e){return angular.isArray(e)?i(s(e)):c(e)},d=function(e){e.active=!0,a.set(e.key,!0)},v=function(e){e.active=!1,a.set(e.key,!1)},y=function(e){e.active=r[e.key],a.remove(e.key)},m=function(){t&&o(t)};return m(),{set:o,get:g,enable:d,disable:v,reset:y,isOn:f,isOnByDefault:u,isOverridden:l}}angular.module("feature-flags",[]),angular.module("feature-flags").directive("featureFlag",["featureFlags","$interpolate",function(e,a){return{transclude:"element",priority:599,terminal:!0,restrict:"A",$$tlb:!0,compile:function(t,r){var n="featureFlagHide"in r;return t[0].textContent=" featureFlag: "+r.featureFlag+" is "+(n?"on":"off")+" ",function(t,r,i,l,f){var u,s;t.$watch(function(){var r=a(i.featureFlag)(t);return e.isOn(r)},function(e){var a=n?!e:e;a?(s=t.$new(),f(s,function(e){u=e,r.after(u).remove()})):(s&&(s.$destroy(),s=null),u&&(u.after(r).remove(),u=null))})}}}}]),angular.module("feature-flags").directive("featureFlagOverrides",["featureFlags",function(e){return{restrict:"A",link:function(a){a.flags=e.get(),a.isOn=e.isOn,a.isOverridden=e.isOverridden,a.enable=e.enable,a.disable=e.disable,a.reset=e.reset,a.isOnByDefault=e.isOnByDefault},template:'

Feature Flags

{{flag.name || flag.key}}
ON
OFF
DEFAULT ({{isOnByDefault(flag.key) ? \'ON\' : \'OFF\'}})
{{flag.description}}
',replace:!0}}]),angular.module("feature-flags").service("featureFlagOverrides",["$rootElement",function(e){var a,t,r=["ng-","data-ng-","ng:","x-ng-",""],n=function(){try{return localStorage.setItem("featureFlags.availableTest","test"),localStorage.removeItem("featureFlags.availableTest"),!0}catch(e){return!1}}(),i=function(e){return t+e},l=function(e){return 0===e.indexOf(t)},f=function(e,a){n&&localStorage.setItem(i(a),e)},u=function(e){return n?localStorage.getItem(i(e)):void 0},s=function(e){n&&localStorage.removeItem(i(e))};return angular.forEach(r,function(t){var r=t+"app";!a&&e.attr(r)&&(a=e.attr(r))}),t="featureFlags."+a+".",{isPresent:function(e){var a=u(e);return"undefined"!=typeof a&&null!==a},get:u,set:function(e,a){angular.isObject(e)?angular.forEach(e,f):f(a,e)},remove:s,reset:function(){var e;if(n)for(e in localStorage)l(e)&&localStorage.removeItem(e)}}}]),angular.module("feature-flags").provider("featureFlags",function(){var a=[];this.setInitialFlags=function(e){a=e},this.$get=["$q","featureFlagOverrides",function(t,r){return new e(t,r,a)}]})}(); \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index a34bd4c..ce7f979 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -12,44 +12,18 @@ var gulp = require('gulp'), concat = require('gulp-concat'), clean = require('gulp-clean'), ngannotate = require('gulp-ng-annotate'), - karma = require('gulp-karma'), ghpages = require('gh-pages'), path = require('path'), gutil = require('gulp-util'), coveralls = require('gulp-coveralls'), pkg = require('./package.json'), + karmaServer = require('karma').Server; //-------------------------------- // HELPERS //-------------------------------- - karmaConfig = function(action) { - return { - frameworks: ['jasmine'], - browsers: ['PhantomJS'], - reporters: ['progress', 'coverage'], - preprocessors: { - 'src/*.js': ['coverage'] - }, - coverageReporter: { - reporters: [{ - type: 'html', - dir: 'test/coverage/' - }, { - type: 'lcov', - dir: 'test/coverage/' - }] - }, - action: action - }; - }, TEST_FILES = 'test/**/*.spec.js', SRC_FILES = 'src/*.js', - KARMA_FILES = [ - 'demo/vendor/angular.min.js', - 'test/vendor/angular-mocks.js', - SRC_FILES, - TEST_FILES - ], PORT = 9999; //-------------------------------- @@ -69,12 +43,8 @@ gulp.task('lint', function() { .pipe(eslint.failOnError()); }); -gulp.task('test', ['clean'], function() { - return gulp.src(KARMA_FILES) - .pipe(karma(karmaConfig('run'))) - .on('error', function(err) { - throw err; - }); +gulp.task('test', ['clean'], function(done) { + return runKarma(done); }); gulp.task('clean', function() { @@ -133,8 +103,7 @@ gulp.task('dev', ['build', 'server'], function() { gulp.watch(['demo/**/*.*'], ['reload']); gulp.watch(['demo/scripts/*.js', TEST_FILES], ['lint']); gulp.watch(SRC_FILES, ['lint', 'build']); - gulp.src(KARMA_FILES) - .pipe(karma(karmaConfig('watch'))); + runKarma(); }); gulp.task('deploy', ['build'], function(done) { @@ -146,3 +115,13 @@ gulp.task('deploy', ['build'], function(done) { gulp.task('precommit', ['lint', 'test', 'build']); gulp.task('demo', ['build', 'server']); gulp.task('default', ['precommit']); + +function runKarma(done) { + new karmaServer({ + configFile: __dirname + '/karma.conf.js' + }, function() { + if (done) { + done(); + } + }).start(); +} diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000..a8f5deb --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,36 @@ +module.exports = function(config) { + config.set({ + files: [ + 'demo/vendor/angular.min.js', + 'test/vendor/angular-mocks.js', + 'src/*.js', + 'test/**/*.spec.js' + ], + frameworks: ['jasmine'], + plugins: [ + require('karma-coverage'), + require('karma-jasmine'), + require('karma-phantomjs-launcher'), + require('karma-mocha-reporter') + ], + browsers: ['PhantomJS'], + reporters: ['mocha', 'coverage'], + preprocessors: { + 'src/*.js': ['coverage'] + }, + coverageReporter: { + reporters: [ + { type: 'text-summary' }, + { + type: 'html', + dir: 'test/coverage/' + }, + { + type: 'lcov', + dir: 'test/coverage/' + } + ] + }, + singleRun: true + }); +}; diff --git a/package.json b/package.json index a8bac47..e4de743 100644 --- a/package.json +++ b/package.json @@ -47,16 +47,17 @@ "gulp-coveralls": "^0.1.2", "gulp-eslint": "^1.0.0", "gulp-header": "^1.0.2", - "gulp-karma": "0.0.4", "gulp-ng-annotate": "^0.3.3", "gulp-rename": "^1.2.0", "gulp-uglify": "^0.2.1", "gulp-util": "^3.0.1", "gulp-wrap": "^0.3.0", - "karma": "^0.12.9", - "karma-coverage": "~0.1.5", - "karma-jasmine": "^0.1.5", - "karma-phantomjs-launcher": "^0.1.4", + "jasmine": "^2.5.2", + "karma": "^1.3.0", + "karma-coverage": "^1.1.1", + "karma-jasmine": "^1.0.2", + "karma-mocha-reporter": "^2.2.0", + "karma-phantomjs-launcher": "^1.0.2", "opn": "^0.1.1" } } diff --git a/src/featureFlagOverrides.service.js b/src/featureFlagOverrides.service.js index 14b8f96..bb16be3 100644 --- a/src/featureFlagOverrides.service.js +++ b/src/featureFlagOverrides.service.js @@ -1,6 +1,7 @@ angular.module('feature-flags').service('featureFlagOverrides', function($rootElement) { - var appName = $rootElement.attr('ng-app'), - keyPrefix = 'featureFlags.' + appName + '.', + var ngAttrPrefixes = ['ng-', 'data-ng-', 'ng:', 'x-ng-', ''], + appName, + keyPrefix, localStorageAvailable = (function() { try { @@ -38,6 +39,16 @@ angular.module('feature-flags').service('featureFlagOverrides', function($rootEl } }; + // Need to find the appName for the angular specific prefixes + angular.forEach(ngAttrPrefixes, function(prefix) { + var name = prefix + 'app'; + if (!appName && $rootElement.attr(name)) { + appName = $rootElement.attr(name); + } + }); + + keyPrefix = 'featureFlags.' + appName + '.'; + return { isPresent: function(key) { var value = get(key); diff --git a/test/featureFlag.directive.spec.js b/test/featureFlag.directive.spec.js index c403b28..40a0851 100644 --- a/test/featureFlag.directive.spec.js +++ b/test/featureFlag.directive.spec.js @@ -23,7 +23,7 @@ describe('when the feature flag', function() { describe('is on', function() { beforeEach(function() { - flagCheck.andReturn(true); + flagCheck.and.returnValue(true); $scope.$digest(); }); @@ -34,7 +34,7 @@ describe('is off', function() { beforeEach(function() { - flagCheck.andReturn(false); + flagCheck.and.returnValue(false); $scope.$digest(); }); @@ -48,9 +48,9 @@ describe('when i toggle it on and off again', function() { beforeEach(function() { - flagCheck.andReturn(true); + flagCheck.and.returnValue(true); $scope.$digest(); - flagCheck.andReturn(false); + flagCheck.and.returnValue(false); $scope.$digest(); }); @@ -100,7 +100,7 @@ describe('when the feature flag', function() { describe('is on', function() { beforeEach(function() { - flagCheck.andReturn(true); + flagCheck.and.returnValue(true); $scope.$digest(); }); @@ -113,7 +113,7 @@ describe('is off', function() { beforeEach(function() { - flagCheck.andReturn(false); + flagCheck.and.returnValue(false); $scope.$digest(); }); @@ -125,9 +125,9 @@ describe('when i toggle it on and off again', function() { beforeEach(function() { - flagCheck.andReturn(true); + flagCheck.and.returnValue(true); $scope.$digest(); - flagCheck.andReturn(false); + flagCheck.and.returnValue(false); $scope.$digest(); }); diff --git a/test/featureFlagOverrides.directive.spec.js b/test/featureFlagOverrides.directive.spec.js index 5c5fec5..9456cff 100644 --- a/test/featureFlagOverrides.directive.spec.js +++ b/test/featureFlagOverrides.directive.spec.js @@ -17,7 +17,7 @@ spyOn(featureFlags, 'enable'); spyOn(featureFlags, 'disable'); spyOn(featureFlags, 'reset'); - spyOn(featureFlags, 'get').andReturn('FLAGS_ARRAY'); + spyOn(featureFlags, 'get').and.returnValue('FLAGS_ARRAY'); container = angular.element('
'); $compile(container)($scope); diff --git a/test/featureFlagOverrides.service.spec.js b/test/featureFlagOverrides.service.spec.js index 6291d26..846eb0e 100644 --- a/test/featureFlagOverrides.service.spec.js +++ b/test/featureFlagOverrides.service.spec.js @@ -2,15 +2,19 @@ 'use strict'; var module = angular.mock.module, - inject = angular.mock.inject; + inject = angular.mock.inject, + appName = 'undefined', + $rootElement = angular.element('
'); describe('Service: featureFlagOverrides', function() { - var service, appName = ''; + var service; - beforeEach(module('feature-flags')); + beforeEach(module('feature-flags', function($provide) { + $provide.value('$rootElement', $rootElement); + })); - beforeEach(inject(function(featureFlagOverrides) { - service = featureFlagOverrides; + beforeEach(inject(function(_featureFlagOverrides_) { + service = _featureFlagOverrides_; })); describe('when I set an override', function() { @@ -66,7 +70,7 @@ describe('when I check the state of an override', function() { describe('if there is one', function() { beforeEach(function() { - spyOn(localStorage, 'getItem').andReturn('true'); + spyOn(localStorage, 'getItem').and.returnValue('true'); }); it('should return true if there is a value', function() { @@ -76,7 +80,7 @@ describe('if there is not one', function() { beforeEach(function() { - spyOn(localStorage, 'getItem').andReturn(null); + spyOn(localStorage, 'getItem').and.returnValue(null); }); it('should return false if there is no value', function() { @@ -115,16 +119,16 @@ var service; beforeEach(function() { - spyOn(localStorage, 'setItem').andThrow(); - spyOn(localStorage, 'getItem').andThrow(); - spyOn(localStorage, 'removeItem').andThrow(); + spyOn(localStorage, 'setItem').and.throwError(); + spyOn(localStorage, 'getItem').and.throwError(); + spyOn(localStorage, 'removeItem').and.throwError(); }); beforeEach(module('feature-flags')); beforeEach(inject(function(featureFlagOverrides) { service = featureFlagOverrides; - localStorage.setItem.reset(); + localStorage.setItem.calls.reset(); })); describe('when I set an override', function() { diff --git a/test/featureFlags.provider.spec.js b/test/featureFlags.provider.spec.js index 99df006..00c252c 100644 --- a/test/featureFlags.provider.spec.js +++ b/test/featureFlags.provider.spec.js @@ -187,7 +187,7 @@ describe('if there is', function() { beforeEach(function() { - spyOn(featureFlagOverrides, 'isPresent').andReturn(true); + spyOn(featureFlagOverrides, 'isPresent').and.returnValue(true); }); it('should return true when there is', function() { @@ -197,7 +197,7 @@ describe('if there is not', function() { beforeEach(function() { - spyOn(featureFlagOverrides, 'isPresent').andReturn(false); + spyOn(featureFlagOverrides, 'isPresent').and.returnValue(false); }); it('should return true when there is', function() { @@ -230,10 +230,10 @@ key: 'FLAG_UNDEFINED_OVERRIDDEN' }; - beforeEach(function(done) { + beforeEach(function() { var flagsToLoad = [onFlag, offFlag, onFlagOverridden, offFlagOverridden]; $httpBackend.when('GET', 'data/flags.json').respond(flagsToLoad); - featureFlags.set($http.get('data/flags.json')).finally(done); + featureFlags.set($http.get('data/flags.json')); $httpBackend.flush(); }); @@ -293,8 +293,8 @@ describe('and there is a local override to turn it on', function() { beforeEach(function() { - spyOn(featureFlagOverrides, 'isPresent').andReturn(true); - spyOn(featureFlagOverrides, 'get').andReturn('true'); + spyOn(featureFlagOverrides, 'isPresent').and.returnValue(true); + spyOn(featureFlagOverrides, 'get').and.returnValue('true'); }); it('should report the feature as being on', function() { @@ -304,7 +304,7 @@ describe('and there is no local override to turn it on', function() { beforeEach(function() { - spyOn(featureFlagOverrides, 'isPresent').andReturn(false); + spyOn(featureFlagOverrides, 'isPresent').and.returnValue(false); }); it('should report the feature as being off', function() { @@ -332,8 +332,8 @@ describe('and there is a local override to turn it off', function() { beforeEach(function() { - spyOn(featureFlagOverrides, 'isPresent').andReturn(true); - spyOn(featureFlagOverrides, 'get').andReturn('false'); + spyOn(featureFlagOverrides, 'isPresent').and.returnValue(true); + spyOn(featureFlagOverrides, 'get').and.returnValue('false'); }); it('should report the feature as being off', function() { @@ -343,7 +343,7 @@ describe('and there is no local override to turn it off', function() { beforeEach(function() { - spyOn(featureFlagOverrides, 'isPresent').andReturn(false); + spyOn(featureFlagOverrides, 'isPresent').and.returnValue(false); }); it('should report the feature as being on', function() { diff --git a/test/vendor/angular-mocks.js b/test/vendor/angular-mocks.js index 7bef5c7..7b3137b 100644 --- a/test/vendor/angular-mocks.js +++ b/test/vendor/angular-mocks.js @@ -1,13 +1,14 @@ /** - * @license AngularJS v1.1.5 - * (c) 2010-2012 Google, Inc. http://angularjs.org + * @license AngularJS v1.2.19 + * (c) 2010-2014 Google, Inc. http://angularjs.org * License: MIT - * - * TODO(vojta): wrap whole file into closure during build */ +(function(window, angular, undefined) { + +'use strict'; /** - * @ngdoc overview + * @ngdoc object * @name angular.mock * @description * @@ -18,7 +19,7 @@ angular.mock = {}; /** * ! This is a private undocumented service ! * - * @name ngMock.$browser + * @name $browser * * @description * This service is a mock implementation of {@link ng.$browser}. It provides fake @@ -29,7 +30,7 @@ angular.mock = {}; * that there are several helper methods available which can be used in tests. */ angular.mock.$BrowserProvider = function() { - this.$get = function(){ + this.$get = function() { return new angular.mock.$Browser(); }; }; @@ -75,6 +76,12 @@ angular.mock.$Browser = function() { }; + /** + * @name $browser#defer.now + * + * @description + * Current milliseconds mock time. + */ self.defer.now = 0; @@ -95,8 +102,7 @@ angular.mock.$Browser = function() { /** - * @name ngMock.$browser#defer.flush - * @methodOf ngMock.$browser + * @name $browser#defer.flush * * @description * Flushes all pending requests and executes the defer callbacks. @@ -110,7 +116,7 @@ angular.mock.$Browser = function() { if (self.deferredFns.length) { self.defer.now = self.deferredFns[self.deferredFns.length-1].time; } else { - throw Error('No deferred tasks to be flushed'); + throw new Error('No deferred tasks to be flushed'); } } @@ -118,13 +124,6 @@ angular.mock.$Browser = function() { self.deferredFns.shift().fn(); } }; - /** - * @name ngMock.$browser#defer.now - * @propertyOf ngMock.$browser - * - * @description - * Current milliseconds mock time. - */ self.$$baseHref = ''; self.baseHref = function() { @@ -134,8 +133,7 @@ angular.mock.$Browser = function() { angular.mock.$Browser.prototype = { /** - * @name ngMock.$browser#poll - * @methodOf ngMock.$browser + * @name $browser#poll * * @description * run all fns in pollFns @@ -162,7 +160,7 @@ angular.mock.$Browser.prototype = { cookies: function(name, value) { if (name) { - if (value == undefined) { + if (angular.isUndefined(value)) { delete this.cookieHash[name]; } else { if (angular.isString(value) && //strings only @@ -186,17 +184,17 @@ angular.mock.$Browser.prototype = { /** - * @ngdoc object - * @name ngMock.$exceptionHandlerProvider + * @ngdoc provider + * @name $exceptionHandlerProvider * * @description - * Configures the mock implementation of {@link ng.$exceptionHandler} to rethrow or to log errors passed - * into the `$exceptionHandler`. + * Configures the mock implementation of {@link ng.$exceptionHandler} to rethrow or to log errors + * passed into the `$exceptionHandler`. */ /** - * @ngdoc object - * @name ngMock.$exceptionHandler + * @ngdoc service + * @name $exceptionHandler * * @description * Mock implementation of {@link ng.$exceptionHandler} that rethrows or logs errors passed @@ -204,7 +202,7 @@ angular.mock.$Browser.prototype = { * information. * * - *
+ * ```js
  *   describe('$exceptionHandlerProvider', function() {
  *
  *     it('should capture log messages and exceptions', function() {
@@ -225,7 +223,7 @@ angular.mock.$Browser.prototype = {
  *       });
  *     });
  *   });
- * 
+ * ``` */ angular.mock.$ExceptionHandlerProvider = function() { @@ -233,8 +231,7 @@ angular.mock.$ExceptionHandlerProvider = function() { /** * @ngdoc method - * @name ngMock.$exceptionHandlerProvider#mode - * @methodOf ngMock.$exceptionHandlerProvider + * @name $exceptionHandlerProvider#mode * * @description * Sets the logging mode. @@ -244,10 +241,10 @@ angular.mock.$ExceptionHandlerProvider = function() { * - `rethrow`: If any errors are passed into the handler in tests, it typically * means that there is a bug in the application or test, so this mock will * make these tests fail. - * - `log`: Sometimes it is desirable to test that an error is thrown, for this case the `log` mode stores an - * array of errors in `$exceptionHandler.errors`, to allow later assertion of them. - * See {@link ngMock.$log#assertEmpty assertEmpty()} and - * {@link ngMock.$log#reset reset()} + * - `log`: Sometimes it is desirable to test that an error is thrown, for this case the `log` + * mode stores an array of errors in `$exceptionHandler.errors`, to allow later + * assertion of them. See {@link ngMock.$log#assertEmpty assertEmpty()} and + * {@link ngMock.$log#reset reset()} */ this.mode = function(mode) { switch(mode) { @@ -270,7 +267,7 @@ angular.mock.$ExceptionHandlerProvider = function() { handler.errors = errors; break; default: - throw Error("Unknown mode '" + mode + "', only 'log'/'rethrow' modes are allowed!"); + throw new Error("Unknown mode '" + mode + "', only 'log'/'rethrow' modes are allowed!"); } }; @@ -284,7 +281,7 @@ angular.mock.$ExceptionHandlerProvider = function() { /** * @ngdoc service - * @name ngMock.$log + * @name $log * * @description * Mock implementation of {@link ng.$log} that gathers all logged messages in arrays @@ -293,24 +290,37 @@ angular.mock.$ExceptionHandlerProvider = function() { * */ angular.mock.$LogProvider = function() { + var debug = true; function concat(array1, array2, index) { return array1.concat(Array.prototype.slice.call(array2, index)); } + this.debugEnabled = function(flag) { + if (angular.isDefined(flag)) { + debug = flag; + return this; + } else { + return debug; + } + }; this.$get = function () { var $log = { log: function() { $log.log.logs.push(concat([], arguments, 0)); }, warn: function() { $log.warn.logs.push(concat([], arguments, 0)); }, info: function() { $log.info.logs.push(concat([], arguments, 0)); }, - error: function() { $log.error.logs.push(concat([], arguments, 0)); } + error: function() { $log.error.logs.push(concat([], arguments, 0)); }, + debug: function() { + if (debug) { + $log.debug.logs.push(concat([], arguments, 0)); + } + } }; /** * @ngdoc method - * @name ngMock.$log#reset - * @methodOf ngMock.$log + * @name $log#reset * * @description * Reset all of the logging arrays to empty. @@ -318,86 +328,97 @@ angular.mock.$LogProvider = function() { $log.reset = function () { /** * @ngdoc property - * @name ngMock.$log#log.logs - * @propertyOf ngMock.$log + * @name $log#log.logs * * @description * Array of messages logged using {@link ngMock.$log#log}. * * @example - *
+       * ```js
        * $log.log('Some Log');
        * var first = $log.log.logs.unshift();
-       * 
+ * ``` */ $log.log.logs = []; /** * @ngdoc property - * @name ngMock.$log#warn.logs - * @propertyOf ngMock.$log + * @name $log#info.logs * * @description - * Array of messages logged using {@link ngMock.$log#warn}. + * Array of messages logged using {@link ngMock.$log#info}. * * @example - *
-       * $log.warn('Some Warning');
-       * var first = $log.warn.logs.unshift();
-       * 
+ * ```js + * $log.info('Some Info'); + * var first = $log.info.logs.unshift(); + * ``` */ - $log.warn.logs = []; + $log.info.logs = []; /** * @ngdoc property - * @name ngMock.$log#info.logs - * @propertyOf ngMock.$log + * @name $log#warn.logs * * @description - * Array of messages logged using {@link ngMock.$log#info}. + * Array of messages logged using {@link ngMock.$log#warn}. * * @example - *
-       * $log.info('Some Info');
-       * var first = $log.info.logs.unshift();
-       * 
+ * ```js + * $log.warn('Some Warning'); + * var first = $log.warn.logs.unshift(); + * ``` */ - $log.info.logs = []; + $log.warn.logs = []; /** * @ngdoc property - * @name ngMock.$log#error.logs - * @propertyOf ngMock.$log + * @name $log#error.logs * * @description * Array of messages logged using {@link ngMock.$log#error}. * * @example - *
-       * $log.log('Some Error');
+       * ```js
+       * $log.error('Some Error');
        * var first = $log.error.logs.unshift();
-       * 
+ * ``` */ $log.error.logs = []; + /** + * @ngdoc property + * @name $log#debug.logs + * + * @description + * Array of messages logged using {@link ngMock.$log#debug}. + * + * @example + * ```js + * $log.debug('Some Error'); + * var first = $log.debug.logs.unshift(); + * ``` + */ + $log.debug.logs = []; }; /** * @ngdoc method - * @name ngMock.$log#assertEmpty - * @methodOf ngMock.$log + * @name $log#assertEmpty * * @description - * Assert that the all of the logging methods have no logged messages. If messages present, an exception is thrown. + * Assert that the all of the logging methods have no logged messages. If messages present, an + * exception is thrown. */ $log.assertEmpty = function() { var errors = []; - angular.forEach(['error', 'warn', 'info', 'log'], function(logLevel) { + angular.forEach(['error', 'warn', 'info', 'log', 'debug'], function(logLevel) { angular.forEach($log[logLevel].logs, function(log) { angular.forEach(log, function (logItem) { - errors.push('MOCK $log (' + logLevel + '): ' + String(logItem) + '\n' + (logItem.stack || '')); + errors.push('MOCK $log (' + logLevel + '): ' + String(logItem) + '\n' + + (logItem.stack || '')); }); }); }); if (errors.length) { - errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or an expected " + - "log message was not checked and removed:"); + errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or "+ + "an expected log message was not checked and removed:"); errors.push(''); throw new Error(errors.join('\n---------\n')); } @@ -409,261 +430,382 @@ angular.mock.$LogProvider = function() { }; -(function() { - var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/; +/** + * @ngdoc service + * @name $interval + * + * @description + * Mock implementation of the $interval service. + * + * Use {@link ngMock.$interval#flush `$interval.flush(millis)`} to + * move forward by `millis` milliseconds and trigger any functions scheduled to run in that + * time. + * + * @param {function()} fn A function that should be called repeatedly. + * @param {number} delay Number of milliseconds between each function call. + * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat + * indefinitely. + * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise + * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. + * @returns {promise} A promise which will be notified on each iteration. + */ +angular.mock.$IntervalProvider = function() { + this.$get = ['$rootScope', '$q', + function($rootScope, $q) { + var repeatFns = [], + nextRepeatId = 0, + now = 0; + + var $interval = function(fn, delay, count, invokeApply) { + var deferred = $q.defer(), + promise = deferred.promise, + iteration = 0, + skipApply = (angular.isDefined(invokeApply) && !invokeApply); + + count = (angular.isDefined(count)) ? count : 0; + promise.then(null, null, fn); + + promise.$$intervalId = nextRepeatId; + + function tick() { + deferred.notify(iteration++); + + if (count > 0 && iteration >= count) { + var fnIndex; + deferred.resolve(iteration); + + angular.forEach(repeatFns, function(fn, index) { + if (fn.id === promise.$$intervalId) fnIndex = index; + }); + + if (fnIndex !== undefined) { + repeatFns.splice(fnIndex, 1); + } + } + + if (!skipApply) $rootScope.$apply(); + } + + repeatFns.push({ + nextTime:(now + delay), + delay: delay, + fn: tick, + id: nextRepeatId, + deferred: deferred + }); + repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;}); + + nextRepeatId++; + return promise; + }; + /** + * @ngdoc method + * @name $interval#cancel + * + * @description + * Cancels a task associated with the `promise`. + * + * @param {promise} promise A promise from calling the `$interval` function. + * @returns {boolean} Returns `true` if the task was successfully cancelled. + */ + $interval.cancel = function(promise) { + if(!promise) return false; + var fnIndex; + + angular.forEach(repeatFns, function(fn, index) { + if (fn.id === promise.$$intervalId) fnIndex = index; + }); + + if (fnIndex !== undefined) { + repeatFns[fnIndex].deferred.reject('canceled'); + repeatFns.splice(fnIndex, 1); + return true; + } + + return false; + }; - function jsonStringToDate(string){ - var match; - if (match = string.match(R_ISO8061_STR)) { - var date = new Date(0), - tzHour = 0, - tzMin = 0; - if (match[9]) { - tzHour = int(match[9] + match[10]); - tzMin = int(match[9] + match[11]); + /** + * @ngdoc method + * @name $interval#flush + * @description + * + * Runs interval tasks scheduled to be run in the next `millis` milliseconds. + * + * @param {number=} millis maximum timeout amount to flush up until. + * + * @return {number} The amount of time moved forward. + */ + $interval.flush = function(millis) { + now += millis; + while (repeatFns.length && repeatFns[0].nextTime <= now) { + var task = repeatFns[0]; + task.fn(); + task.nextTime += task.delay; + repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;}); } - date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3])); - date.setUTCHours(int(match[4]||0) - tzHour, int(match[5]||0) - tzMin, int(match[6]||0), int(match[7]||0)); - return date; + return millis; + }; + + return $interval; + }]; +}; + + +/* jshint -W101 */ +/* The R_ISO8061_STR regex is never going to fit into the 100 char limit! + * This directive should go inside the anonymous function but a bug in JSHint means that it would + * not be enacted early enough to prevent the warning. + */ +var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/; + +function jsonStringToDate(string) { + var match; + if (match = string.match(R_ISO8061_STR)) { + var date = new Date(0), + tzHour = 0, + tzMin = 0; + if (match[9]) { + tzHour = int(match[9] + match[10]); + tzMin = int(match[9] + match[11]); } - return string; + date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3])); + date.setUTCHours(int(match[4]||0) - tzHour, + int(match[5]||0) - tzMin, + int(match[6]||0), + int(match[7]||0)); + return date; } + return string; +} - function int(str) { - return parseInt(str, 10); +function int(str) { + return parseInt(str, 10); +} + +function padNumber(num, digits, trim) { + var neg = ''; + if (num < 0) { + neg = '-'; + num = -num; } + num = '' + num; + while(num.length < digits) num = '0' + num; + if (trim) + num = num.substr(num.length - digits); + return neg + num; +} - function padNumber(num, digits, trim) { - var neg = ''; - if (num < 0) { - neg = '-'; - num = -num; - } - num = '' + num; - while(num.length < digits) num = '0' + num; - if (trim) - num = num.substr(num.length - digits); - return neg + num; + +/** + * @ngdoc type + * @name angular.mock.TzDate + * @description + * + * *NOTE*: this is not an injectable instance, just a globally available mock class of `Date`. + * + * Mock of the Date type which has its timezone specified via constructor arg. + * + * The main purpose is to create Date-like instances with timezone fixed to the specified timezone + * offset, so that we can test code that depends on local timezone settings without dependency on + * the time zone settings of the machine where the code is running. + * + * @param {number} offset Offset of the *desired* timezone in hours (fractions will be honored) + * @param {(number|string)} timestamp Timestamp representing the desired time in *UTC* + * + * @example + * !!!! WARNING !!!!! + * This is not a complete Date object so only methods that were implemented can be called safely. + * To make matters worse, TzDate instances inherit stuff from Date via a prototype. + * + * We do our best to intercept calls to "unimplemented" methods, but since the list of methods is + * incomplete we might be missing some non-standard methods. This can result in errors like: + * "Date.prototype.foo called on incompatible Object". + * + * ```js + * var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z'); + * newYearInBratislava.getTimezoneOffset() => -60; + * newYearInBratislava.getFullYear() => 2010; + * newYearInBratislava.getMonth() => 0; + * newYearInBratislava.getDate() => 1; + * newYearInBratislava.getHours() => 0; + * newYearInBratislava.getMinutes() => 0; + * newYearInBratislava.getSeconds() => 0; + * ``` + * + */ +angular.mock.TzDate = function (offset, timestamp) { + var self = new Date(0); + if (angular.isString(timestamp)) { + var tsStr = timestamp; + + self.origDate = jsonStringToDate(timestamp); + + timestamp = self.origDate.getTime(); + if (isNaN(timestamp)) + throw { + name: "Illegal Argument", + message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string" + }; + } else { + self.origDate = new Date(timestamp); } + var localOffset = new Date(timestamp).getTimezoneOffset(); + self.offsetDiff = localOffset*60*1000 - offset*1000*60*60; + self.date = new Date(timestamp + self.offsetDiff); - /** - * @ngdoc object - * @name angular.mock.TzDate - * @description - * - * *NOTE*: this is not an injectable instance, just a globally available mock class of `Date`. - * - * Mock of the Date type which has its timezone specified via constructor arg. - * - * The main purpose is to create Date-like instances with timezone fixed to the specified timezone - * offset, so that we can test code that depends on local timezone settings without dependency on - * the time zone settings of the machine where the code is running. - * - * @param {number} offset Offset of the *desired* timezone in hours (fractions will be honored) - * @param {(number|string)} timestamp Timestamp representing the desired time in *UTC* - * - * @example - * !!!! WARNING !!!!! - * This is not a complete Date object so only methods that were implemented can be called safely. - * To make matters worse, TzDate instances inherit stuff from Date via a prototype. - * - * We do our best to intercept calls to "unimplemented" methods, but since the list of methods is - * incomplete we might be missing some non-standard methods. This can result in errors like: - * "Date.prototype.foo called on incompatible Object". - * - *
-   * var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z');
-   * newYearInBratislava.getTimezoneOffset() => -60;
-   * newYearInBratislava.getFullYear() => 2010;
-   * newYearInBratislava.getMonth() => 0;
-   * newYearInBratislava.getDate() => 1;
-   * newYearInBratislava.getHours() => 0;
-   * newYearInBratislava.getMinutes() => 0;
-   * newYearInBratislava.getSeconds() => 0;
-   * 
- * - */ - angular.mock.TzDate = function (offset, timestamp) { - var self = new Date(0); - if (angular.isString(timestamp)) { - var tsStr = timestamp; - - self.origDate = jsonStringToDate(timestamp); - - timestamp = self.origDate.getTime(); - if (isNaN(timestamp)) - throw { - name: "Illegal Argument", - message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string" - }; - } else { - self.origDate = new Date(timestamp); - } + self.getTime = function() { + return self.date.getTime() - self.offsetDiff; + }; - var localOffset = new Date(timestamp).getTimezoneOffset(); - self.offsetDiff = localOffset*60*1000 - offset*1000*60*60; - self.date = new Date(timestamp + self.offsetDiff); + self.toLocaleDateString = function() { + return self.date.toLocaleDateString(); + }; - self.getTime = function() { - return self.date.getTime() - self.offsetDiff; - }; + self.getFullYear = function() { + return self.date.getFullYear(); + }; - self.toLocaleDateString = function() { - return self.date.toLocaleDateString(); - }; + self.getMonth = function() { + return self.date.getMonth(); + }; - self.getFullYear = function() { - return self.date.getFullYear(); - }; + self.getDate = function() { + return self.date.getDate(); + }; - self.getMonth = function() { - return self.date.getMonth(); - }; + self.getHours = function() { + return self.date.getHours(); + }; - self.getDate = function() { - return self.date.getDate(); - }; + self.getMinutes = function() { + return self.date.getMinutes(); + }; - self.getHours = function() { - return self.date.getHours(); - }; + self.getSeconds = function() { + return self.date.getSeconds(); + }; - self.getMinutes = function() { - return self.date.getMinutes(); - }; + self.getMilliseconds = function() { + return self.date.getMilliseconds(); + }; - self.getSeconds = function() { - return self.date.getSeconds(); - }; + self.getTimezoneOffset = function() { + return offset * 60; + }; - self.getMilliseconds = function() { - return self.date.getMilliseconds(); - }; + self.getUTCFullYear = function() { + return self.origDate.getUTCFullYear(); + }; - self.getTimezoneOffset = function() { - return offset * 60; - }; + self.getUTCMonth = function() { + return self.origDate.getUTCMonth(); + }; - self.getUTCFullYear = function() { - return self.origDate.getUTCFullYear(); - }; + self.getUTCDate = function() { + return self.origDate.getUTCDate(); + }; - self.getUTCMonth = function() { - return self.origDate.getUTCMonth(); - }; + self.getUTCHours = function() { + return self.origDate.getUTCHours(); + }; - self.getUTCDate = function() { - return self.origDate.getUTCDate(); - }; + self.getUTCMinutes = function() { + return self.origDate.getUTCMinutes(); + }; - self.getUTCHours = function() { - return self.origDate.getUTCHours(); - }; + self.getUTCSeconds = function() { + return self.origDate.getUTCSeconds(); + }; - self.getUTCMinutes = function() { - return self.origDate.getUTCMinutes(); - }; + self.getUTCMilliseconds = function() { + return self.origDate.getUTCMilliseconds(); + }; - self.getUTCSeconds = function() { - return self.origDate.getUTCSeconds(); - }; + self.getDay = function() { + return self.date.getDay(); + }; - self.getUTCMilliseconds = function() { - return self.origDate.getUTCMilliseconds(); + // provide this method only on browsers that already have it + if (self.toISOString) { + self.toISOString = function() { + return padNumber(self.origDate.getUTCFullYear(), 4) + '-' + + padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' + + padNumber(self.origDate.getUTCDate(), 2) + 'T' + + padNumber(self.origDate.getUTCHours(), 2) + ':' + + padNumber(self.origDate.getUTCMinutes(), 2) + ':' + + padNumber(self.origDate.getUTCSeconds(), 2) + '.' + + padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z'; }; + } - self.getDay = function() { - return self.date.getDay(); + //hide all methods not implemented in this mock that the Date prototype exposes + var unimplementedMethods = ['getUTCDay', + 'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds', + 'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear', + 'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds', + 'setYear', 'toDateString', 'toGMTString', 'toJSON', 'toLocaleFormat', 'toLocaleString', + 'toLocaleTimeString', 'toSource', 'toString', 'toTimeString', 'toUTCString', 'valueOf']; + + angular.forEach(unimplementedMethods, function(methodName) { + self[methodName] = function() { + throw new Error("Method '" + methodName + "' is not implemented in the TzDate mock"); }; + }); - // provide this method only on browsers that already have it - if (self.toISOString) { - self.toISOString = function() { - return padNumber(self.origDate.getUTCFullYear(), 4) + '-' + - padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' + - padNumber(self.origDate.getUTCDate(), 2) + 'T' + - padNumber(self.origDate.getUTCHours(), 2) + ':' + - padNumber(self.origDate.getUTCMinutes(), 2) + ':' + - padNumber(self.origDate.getUTCSeconds(), 2) + '.' + - padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z' - } - } + return self; +}; - //hide all methods not implemented in this mock that the Date prototype exposes - var unimplementedMethods = ['getUTCDay', - 'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds', - 'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear', - 'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds', - 'setYear', 'toDateString', 'toGMTString', 'toJSON', 'toLocaleFormat', 'toLocaleString', - 'toLocaleTimeString', 'toSource', 'toString', 'toTimeString', 'toUTCString', 'valueOf']; - - angular.forEach(unimplementedMethods, function(methodName) { - self[methodName] = function() { - throw Error("Method '" + methodName + "' is not implemented in the TzDate mock"); - }; - }); +//make "tzDateInstance instanceof Date" return true +angular.mock.TzDate.prototype = Date.prototype; +/* jshint +W101 */ - return self; - }; +angular.mock.animate = angular.module('ngAnimateMock', ['ng']) - //make "tzDateInstance instanceof Date" return true - angular.mock.TzDate.prototype = Date.prototype; -})(); + .config(['$provide', function($provide) { -/** - * @ngdoc function - * @name angular.mock.createMockWindow - * @description - * - * This function creates a mock window object useful for controlling access ot setTimeout, but mocking out - * sufficient window's properties to allow Angular to execute. - * - * @example - * - *
-    beforeEach(module(function($provide) {
-      $provide.value('$window', window = angular.mock.createMockWindow());
-    }));
-
-    it('should do something', inject(function($window) {
-      var val = null;
-      $window.setTimeout(function() { val = 123; }, 10);
-      expect(val).toEqual(null);
-      window.setTimeout.expect(10).process();
-      expect(val).toEqual(123);
+    var reflowQueue = [];
+    $provide.value('$$animateReflow', function(fn) {
+      var index = reflowQueue.length;
+      reflowQueue.push(fn);
+      return function cancel() {
+        reflowQueue.splice(index, 1);
+      };
     });
- * 
- * - */ -angular.mock.createMockWindow = function() { - var mockWindow = {}; - var setTimeoutQueue = []; - - mockWindow.document = window.document; - mockWindow.getComputedStyle = angular.bind(window, window.getComputedStyle); - mockWindow.scrollTo = angular.bind(window, window.scrollTo); - mockWindow.navigator = window.navigator; - mockWindow.setTimeout = function(fn, delay) { - setTimeoutQueue.push({fn: fn, delay: delay}); - }; - mockWindow.setTimeout.queue = setTimeoutQueue; - mockWindow.setTimeout.expect = function(delay) { - if (setTimeoutQueue.length > 0) { - return { - process: function() { - var tick = setTimeoutQueue.shift(); - expect(tick.delay).toEqual(delay); - tick.fn(); + + $provide.decorator('$animate', function($delegate, $$asyncCallback) { + var animate = { + queue : [], + enabled : $delegate.enabled, + triggerCallbacks : function() { + $$asyncCallback.flush(); + }, + triggerReflow : function() { + angular.forEach(reflowQueue, function(fn) { + fn(); + }); + reflowQueue = []; } }; - } else { - expect('SetTimoutQueue empty. Expecting delay of ').toEqual(delay); - } - }; - return mockWindow; -}; + angular.forEach( + ['enter','leave','move','addClass','removeClass','setClass'], function(method) { + animate[method] = function() { + animate.queue.push({ + event : method, + element : arguments[0], + args : arguments + }); + $delegate[method].apply($delegate, arguments); + }; + }); + + return animate; + }); + + }]); + /** * @ngdoc function @@ -672,9 +814,11 @@ angular.mock.createMockWindow = function() { * * *NOTE*: this is not an injectable instance, just a globally available function. * - * Method for serializing common angular objects (scope, elements, etc..) into strings, useful for debugging. + * Method for serializing common angular objects (scope, elements, etc..) into strings, useful for + * debugging. * - * This method is also available on window, where it can be used to display objects on debug console. + * This method is also available on window, where it can be used to display objects on debug + * console. * * @param {*} object - any object to turn into string. * @return {string} a serialized string of the argument @@ -704,6 +848,8 @@ angular.mock.dump = function(object) { } else if (object instanceof Error) { out = object.stack || ('' + object.name + ': ' + object.message); } else { + // TODO(i): this prevents methods being logged, + // we should have a better way to serialize objects out = angular.toJson(object, true); } } else { @@ -717,7 +863,7 @@ angular.mock.dump = function(object) { offset = offset || ' '; var log = [offset + 'Scope(' + scope.$id + '): {']; for ( var key in scope ) { - if (scope.hasOwnProperty(key) && !key.match(/^(\$|this)/)) { + if (Object.prototype.hasOwnProperty.call(scope, key) && !key.match(/^(\$|this)/)) { log.push(' ' + key + ': ' + angular.toJson(scope[key])); } } @@ -732,8 +878,8 @@ angular.mock.dump = function(object) { }; /** - * @ngdoc object - * @name ngMock.$httpBackend + * @ngdoc service + * @name $httpBackend * @description * Fake HTTP backend implementation suitable for unit testing applications that use the * {@link ng.$http $http service}. @@ -742,8 +888,8 @@ angular.mock.dump = function(object) { * development please see {@link ngMockE2E.$httpBackend e2e $httpBackend mock}. * * During unit testing, we want our unit tests to run quickly and have no external dependencies so - * we don’t want to send {@link https://developer.mozilla.org/en/xmlhttprequest XHR} or - * {@link http://en.wikipedia.org/wiki/JSONP JSONP} requests to a real server. All we really need is + * we don’t want to send [XHR](https://developer.mozilla.org/en/xmlhttprequest) or + * [JSONP](http://en.wikipedia.org/wiki/JSONP) requests to a real server. All we really need is * to verify whether a certain request has been sent or not, or alternatively just let the * application make requests, respond with pre-trained responses and assert that the end result is * what we expect it to be. @@ -754,7 +900,7 @@ angular.mock.dump = function(object) { * When an Angular application needs some data from a server, it calls the $http service, which * sends the request to a real server using $httpBackend service. With dependency injection, it is * easy to inject $httpBackend mock (which has the same API as $httpBackend) and use it to verify - * the requests and respond with some testing data without sending a request to real server. + * the requests and respond with some testing data without sending a request to a real server. * * There are two ways to specify what test data should be returned as http responses by the mock * backend when the code under test makes http requests: @@ -821,86 +967,110 @@ angular.mock.dump = function(object) { * * # Flushing HTTP requests * - * The $httpBackend used in production, always responds to requests with responses asynchronously. - * If we preserved this behavior in unit testing, we'd have to create async unit tests, which are - * hard to write, follow and maintain. At the same time the testing mock, can't respond - * synchronously because that would change the execution of the code under test. For this reason the - * mock $httpBackend has a `flush()` method, which allows the test to explicitly flush pending - * requests and thus preserving the async api of the backend, while allowing the test to execute - * synchronously. + * The $httpBackend used in production always responds to requests asynchronously. If we preserved + * this behavior in unit testing, we'd have to create async unit tests, which are hard to write, + * to follow and to maintain. But neither can the testing mock respond synchronously; that would + * change the execution of the code under test. For this reason, the mock $httpBackend has a + * `flush()` method, which allows the test to explicitly flush pending requests. This preserves + * the async api of the backend, while allowing the test to execute synchronously. * * * # Unit testing with mock $httpBackend + * The following code shows how to setup and use the mock backend when unit testing a controller. + * First we create the controller under test: * - *
-   // controller
-   function MyController($scope, $http) {
-     $http.get('/auth.py').success(function(data) {
-       $scope.user = data;
-     });
-
-     this.saveMessage = function(message) {
-       $scope.status = 'Saving...';
-       $http.post('/add-msg.py', message).success(function(response) {
-         $scope.status = '';
-       }).error(function() {
-         $scope.status = 'ERROR!';
-       });
-     };
-   }
+  ```js
+  // The controller code
+  function MyController($scope, $http) {
+    var authToken;
 
-   // testing controller
-   var $httpBackend;
+    $http.get('/auth.py').success(function(data, status, headers) {
+      authToken = headers('A-Token');
+      $scope.user = data;
+    });
 
-   beforeEach(inject(function($injector) {
-     $httpBackend = $injector.get('$httpBackend');
+    $scope.saveMessage = function(message) {
+      var headers = { 'Authorization': authToken };
+      $scope.status = 'Saving...';
 
-     // backend definition common for all tests
-     $httpBackend.when('GET', '/auth.py').respond({userId: 'userX'}, {'A-Token': 'xxx'});
-   }));
+      $http.post('/add-msg.py', message, { headers: headers } ).success(function(response) {
+        $scope.status = '';
+      }).error(function() {
+        $scope.status = 'ERROR!';
+      });
+    };
+  }
+  ```
+ *
+ * Now we setup the mock backend and create the test specs:
+ *
+  ```js
+    // testing controller
+    describe('MyController', function() {
+       var $httpBackend, $rootScope, createController;
 
+       beforeEach(inject(function($injector) {
+         // Set up the mock http service responses
+         $httpBackend = $injector.get('$httpBackend');
+         // backend definition common for all tests
+         $httpBackend.when('GET', '/auth.py').respond({userId: 'userX'}, {'A-Token': 'xxx'});
 
-   afterEach(function() {
-     $httpBackend.verifyNoOutstandingExpectation();
-     $httpBackend.verifyNoOutstandingRequest();
-   });
+         // Get hold of a scope (i.e. the root scope)
+         $rootScope = $injector.get('$rootScope');
+         // The $controller service is used to create instances of controllers
+         var $controller = $injector.get('$controller');
 
+         createController = function() {
+           return $controller('MyController', {'$scope' : $rootScope });
+         };
+       }));
 
-   it('should fetch authentication token', function() {
-     $httpBackend.expectGET('/auth.py');
-     var controller = scope.$new(MyController);
-     $httpBackend.flush();
-   });
 
+       afterEach(function() {
+         $httpBackend.verifyNoOutstandingExpectation();
+         $httpBackend.verifyNoOutstandingRequest();
+       });
 
-   it('should send msg to server', function() {
-     // now you don’t care about the authentication, but
-     // the controller will still send the request and
-     // $httpBackend will respond without you having to
-     // specify the expectation and response for this request
-     $httpBackend.expectPOST('/add-msg.py', 'message content').respond(201, '');
 
-     var controller = scope.$new(MyController);
-     $httpBackend.flush();
-     controller.saveMessage('message content');
-     expect(controller.status).toBe('Saving...');
-     $httpBackend.flush();
-     expect(controller.status).toBe('');
-   });
+       it('should fetch authentication token', function() {
+         $httpBackend.expectGET('/auth.py');
+         var controller = createController();
+         $httpBackend.flush();
+       });
+
+
+       it('should send msg to server', function() {
+         var controller = createController();
+         $httpBackend.flush();
+
+         // now you don’t care about the authentication, but
+         // the controller will still send the request and
+         // $httpBackend will respond without you having to
+         // specify the expectation and response for this request
+
+         $httpBackend.expectPOST('/add-msg.py', 'message content').respond(201, '');
+         $rootScope.saveMessage('message content');
+         expect($rootScope.status).toBe('Saving...');
+         $httpBackend.flush();
+         expect($rootScope.status).toBe('');
+       });
 
 
-   it('should send auth header', function() {
-     $httpBackend.expectPOST('/add-msg.py', undefined, function(headers) {
-       // check if the header was send, if it wasn't the expectation won't
-       // match the request and the test will fail
-       return headers['Authorization'] == 'xxx';
-     }).respond(201, '');
+       it('should send auth header', function() {
+         var controller = createController();
+         $httpBackend.flush();
 
-     var controller = scope.$new(MyController);
-     controller.saveMessage('whatever');
-     $httpBackend.flush();
-   });
-   
+ $httpBackend.expectPOST('/add-msg.py', undefined, function(headers) { + // check if the header was send, if it wasn't the expectation won't + // match the request and the test will fail + return headers['Authorization'] == 'xxx'; + }).respond(201, ''); + + $rootScope.saveMessage('whatever'); + $httpBackend.flush(); + }); + }); + ``` */ angular.mock.$HttpBackendProvider = function() { this.$get = ['$rootScope', createHttpBackendMock]; @@ -924,20 +1094,21 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { var definitions = [], expectations = [], responses = [], - responsesPush = angular.bind(responses, responses.push); + responsesPush = angular.bind(responses, responses.push), + copy = angular.copy; - function createResponse(status, data, headers) { + function createResponse(status, data, headers, statusText) { if (angular.isFunction(status)) return status; return function() { return angular.isNumber(status) - ? [status, data, headers] + ? [status, data, headers, statusText] : [200, status, data]; }; } // TODO(vojta): change params to: method, url, data, headers, callback - function $httpBackend(method, url, data, callback, headers, timeout) { + function $httpBackend(method, url, data, callback, headers, timeout, withCredentials) { var xhr = new MockXhr(), expectation = expectations[0], wasExpected = false; @@ -956,7 +1127,8 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { function handleResponse() { var response = wrapped.response(method, url, data, headers); xhr.$$respHeaders = response[2]; - callback(response[0], response[1], xhr.getAllResponseHeaders()); + callback(copy(response[0]), copy(response[1]), xhr.getAllResponseHeaders(), + copy(response[3] || '')); } function handleTimeout() { @@ -972,13 +1144,13 @@ 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(); @@ -996,42 +1168,44 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { // if $browser specified, we do auto flush all requests ($browser ? $browser.defer : responsesPush)(wrapResponse(definition)); } else if (definition.passThrough) { - $delegate(method, url, data, callback, headers, timeout); - } else throw Error('No response defined !'); + $delegate(method, url, data, callback, headers, timeout, withCredentials); + } else throw new Error('No response defined !'); return; } } 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')); } /** * @ngdoc method - * @name ngMock.$httpBackend#when - * @methodOf ngMock.$httpBackend + * @name $httpBackend#when * @description * Creates a new backend definition. * * @param {string} method HTTP method. * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. + * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives + * data string and returns true if the data is as expected. * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header * object and returns true if the headers match the current definition. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. * - * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` - * – The respond method takes a set of static data to be returned or a function that can return - * an array containing response status (number), response data (string) and response headers - * (Object). + * - respond – + * `{function([status,] data[, headers, statusText]) + * | function(function(method, url, data, headers)}` + * – The respond method takes a set of static data to be returned or a function that can + * return an array containing response status (number), response data (string), response + * headers (Object), and the text for the status (string). */ $httpBackend.when = function(method, url, data, headers) { var definition = new MockHttpExpectation(method, url, data, headers), chain = { - respond: function(status, data, headers) { - definition.response = createResponse(status, data, headers); + respond: function(status, data, headers, statusText) { + definition.response = createResponse(status, data, headers, statusText); } }; @@ -1047,8 +1221,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { /** * @ngdoc method - * @name ngMock.$httpBackend#whenGET - * @methodOf ngMock.$httpBackend + * @name $httpBackend#whenGET * @description * Creates a new backend definition for GET requests. For more info see `when()`. * @@ -1060,8 +1233,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { /** * @ngdoc method - * @name ngMock.$httpBackend#whenHEAD - * @methodOf ngMock.$httpBackend + * @name $httpBackend#whenHEAD * @description * Creates a new backend definition for HEAD requests. For more info see `when()`. * @@ -1073,8 +1245,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { /** * @ngdoc method - * @name ngMock.$httpBackend#whenDELETE - * @methodOf ngMock.$httpBackend + * @name $httpBackend#whenDELETE * @description * Creates a new backend definition for DELETE requests. For more info see `when()`. * @@ -1086,13 +1257,13 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { /** * @ngdoc method - * @name ngMock.$httpBackend#whenPOST - * @methodOf ngMock.$httpBackend + * @name $httpBackend#whenPOST * @description * Creates a new backend definition for POST requests. For more info see `when()`. * * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. + * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives + * data string and returns true if the data is as expected. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that control how a matched * request is handled. @@ -1100,13 +1271,13 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { /** * @ngdoc method - * @name ngMock.$httpBackend#whenPUT - * @methodOf ngMock.$httpBackend + * @name $httpBackend#whenPUT * @description * Creates a new backend definition for PUT requests. For more info see `when()`. * * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. + * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives + * data string and returns true if the data is as expected. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that control how a matched * request is handled. @@ -1114,8 +1285,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { /** * @ngdoc method - * @name ngMock.$httpBackend#whenJSONP - * @methodOf ngMock.$httpBackend + * @name $httpBackend#whenJSONP * @description * Creates a new backend definition for JSONP requests. For more info see `when()`. * @@ -1128,30 +1298,33 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { /** * @ngdoc method - * @name ngMock.$httpBackend#expect - * @methodOf ngMock.$httpBackend + * @name $httpBackend#expect * @description * Creates a new request expectation. * * @param {string} method HTTP method. * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. + * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that + * receives data string and returns true if the data is as expected, or Object if request body + * is in JSON format. * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header * object and returns true if the headers match the current expectation. * @returns {requestHandler} Returns an object with `respond` method that control how a matched * request is handled. * - * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` - * – The respond method takes a set of static data to be returned or a function that can return - * an array containing response status (number), response data (string) and response headers - * (Object). + * - respond – + * `{function([status,] data[, headers, statusText]) + * | function(function(method, url, data, headers)}` + * – The respond method takes a set of static data to be returned or a function that can + * return an array containing response status (number), response data (string), response + * headers (Object), and the text for the status (string). */ $httpBackend.expect = function(method, url, data, headers) { var expectation = new MockHttpExpectation(method, url, data, headers); expectations.push(expectation); return { - respond: function(status, data, headers) { - expectation.response = createResponse(status, data, headers); + respond: function (status, data, headers, statusText) { + expectation.response = createResponse(status, data, headers, statusText); } }; }; @@ -1159,8 +1332,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { /** * @ngdoc method - * @name ngMock.$httpBackend#expectGET - * @methodOf ngMock.$httpBackend + * @name $httpBackend#expectGET * @description * Creates a new request expectation for GET requests. For more info see `expect()`. * @@ -1172,8 +1344,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { /** * @ngdoc method - * @name ngMock.$httpBackend#expectHEAD - * @methodOf ngMock.$httpBackend + * @name $httpBackend#expectHEAD * @description * Creates a new request expectation for HEAD requests. For more info see `expect()`. * @@ -1185,8 +1356,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { /** * @ngdoc method - * @name ngMock.$httpBackend#expectDELETE - * @methodOf ngMock.$httpBackend + * @name $httpBackend#expectDELETE * @description * Creates a new request expectation for DELETE requests. For more info see `expect()`. * @@ -1198,13 +1368,14 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { /** * @ngdoc method - * @name ngMock.$httpBackend#expectPOST - * @methodOf ngMock.$httpBackend + * @name $httpBackend#expectPOST * @description * Creates a new request expectation for POST requests. For more info see `expect()`. * * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. + * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that + * receives data string and returns true if the data is as expected, or Object if request body + * is in JSON format. * @param {Object=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that control how a matched * request is handled. @@ -1212,13 +1383,14 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { /** * @ngdoc method - * @name ngMock.$httpBackend#expectPUT - * @methodOf ngMock.$httpBackend + * @name $httpBackend#expectPUT * @description * Creates a new request expectation for PUT requests. For more info see `expect()`. * * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. + * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that + * receives data string and returns true if the data is as expected, or Object if request body + * is in JSON format. * @param {Object=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that control how a matched * request is handled. @@ -1226,13 +1398,14 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { /** * @ngdoc method - * @name ngMock.$httpBackend#expectPATCH - * @methodOf ngMock.$httpBackend + * @name $httpBackend#expectPATCH * @description * Creates a new request expectation for PATCH requests. For more info see `expect()`. * * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. + * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that + * receives data string and returns true if the data is as expected, or Object if request body + * is in JSON format. * @param {Object=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that control how a matched * request is handled. @@ -1240,8 +1413,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { /** * @ngdoc method - * @name ngMock.$httpBackend#expectJSONP - * @methodOf ngMock.$httpBackend + * @name $httpBackend#expectJSONP * @description * Creates a new request expectation for JSONP requests. For more info see `expect()`. * @@ -1254,8 +1426,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { /** * @ngdoc method - * @name ngMock.$httpBackend#flush - * @methodOf ngMock.$httpBackend + * @name $httpBackend#flush * @description * Flushes all pending requests using the trained responses. * @@ -1265,11 +1436,11 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { */ $httpBackend.flush = function(count) { $rootScope.$digest(); - if (!responses.length) throw Error('No pending request to flush !'); + if (!responses.length) throw new Error('No pending request to flush !'); if (angular.isDefined(count)) { while (count--) { - if (!responses.length) throw Error('No more pending request to flush !'); + if (!responses.length) throw new Error('No more pending request to flush !'); responses.shift()(); } } else { @@ -1283,8 +1454,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { /** * @ngdoc method - * @name ngMock.$httpBackend#verifyNoOutstandingExpectation - * @methodOf ngMock.$httpBackend + * @name $httpBackend#verifyNoOutstandingExpectation * @description * Verifies that all of the requests defined via the `expect` api were made. If any of the * requests were not made, verifyNoOutstandingExpectation throws an exception. @@ -1292,43 +1462,41 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * Typically, you would call this method following each test case that asserts requests using an * "afterEach" clause. * - *
-   *   afterEach($httpBackend.verifyExpectations);
-   * 
+ * ```js + * afterEach($httpBackend.verifyNoOutstandingExpectation); + * ``` */ $httpBackend.verifyNoOutstandingExpectation = function() { $rootScope.$digest(); if (expectations.length) { - throw Error('Unsatisfied requests: ' + expectations.join(', ')); + throw new Error('Unsatisfied requests: ' + expectations.join(', ')); } }; /** * @ngdoc method - * @name ngMock.$httpBackend#verifyNoOutstandingRequest - * @methodOf ngMock.$httpBackend + * @name $httpBackend#verifyNoOutstandingRequest * @description * Verifies that there are no outstanding requests that need to be flushed. * * Typically, you would call this method following each test case that asserts requests using an * "afterEach" clause. * - *
+   * ```js
    *   afterEach($httpBackend.verifyNoOutstandingRequest);
-   * 
+ * ``` */ $httpBackend.verifyNoOutstandingRequest = function() { if (responses.length) { - throw Error('Unflushed requests: ' + responses.length); + throw new Error('Unflushed requests: ' + responses.length); } }; /** * @ngdoc method - * @name ngMock.$httpBackend#resetExpectations - * @methodOf ngMock.$httpBackend + * @name $httpBackend#resetExpectations * @description * Resets all request expectations, but preserves all backend definitions. Typically, you would * call resetExpectations during a multiple-phase test when you want to reuse the same instance of @@ -1345,14 +1513,14 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { function createShortMethods(prefix) { angular.forEach(['GET', 'DELETE', 'JSONP'], function(method) { $httpBackend[prefix + method] = function(url, headers) { - return $httpBackend[prefix](method, url, undefined, headers) - } + return $httpBackend[prefix](method, url, undefined, headers); + }; }); angular.forEach(['PUT', 'POST', 'PATCH'], function(method) { $httpBackend[prefix + method] = function(url, data, headers) { - return $httpBackend[prefix](method, url, data, headers) - } + return $httpBackend[prefix](method, url, data, headers); + }; }); } } @@ -1385,7 +1553,8 @@ function MockHttpExpectation(method, url, data, headers) { this.matchData = function(d) { if (angular.isUndefined(data)) return true; if (data && angular.isFunction(data.test)) return data.test(d); - if (data && !angular.isString(data)) return angular.toJson(data) == d; + if (data && angular.isFunction(data)) return data(d); + if (data && !angular.isString(data)) return angular.equals(data, angular.fromJson(d)); return data == d; }; @@ -1394,6 +1563,10 @@ function MockHttpExpectation(method, url, data, headers) { }; } +function createMockXhr() { + return new MockXhr(); +} + function MockXhr() { // hack for testing $http, $httpBackend @@ -1416,7 +1589,8 @@ function MockXhr() { }; this.getResponseHeader = function(name) { - // the lookup must be case insensitive, that's why we try two quick lookups and full scan at last + // the lookup must be case insensitive, + // that's why we try two quick lookups first and full scan last var header = this.$$respHeaders[name]; if (header) return header; @@ -1445,39 +1619,39 @@ function MockXhr() { /** - * @ngdoc function - * @name ngMock.$timeout + * @ngdoc service + * @name $timeout * @description * * 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) { /** * @ngdoc method - * @name ngMock.$timeout#flush - * @methodOf ngMock.$timeout + * @name $timeout#flush * @description * * Flushes the queue of pending tasks. + * + * @param {number=} delay maximum timeout amount to flush up until */ - $delegate.flush = function() { - $browser.defer.flush(); + $delegate.flush = function(delay) { + $browser.defer.flush(delay); }; /** * @ngdoc method - * @name ngMock.$timeout#verifyNoPendingTasks - * @methodOf ngMock.$timeout + * @name $timeout#verifyNoPendingTasks * @description * * Verifies that there are no pending tasks that need to be flushed. */ $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)); } }; @@ -1494,49 +1668,103 @@ angular.mock.$TimeoutDecorator = function($delegate, $browser) { return $delegate; }; +angular.mock.$RAFDecorator = function($delegate) { + var queue = []; + var rafFn = function(fn) { + var index = queue.length; + queue.push(fn); + return function() { + queue.splice(index, 1); + }; + }; + + rafFn.supported = $delegate.supported; + + rafFn.flush = function() { + if(queue.length === 0) { + throw new Error('No rAF callbacks present'); + } + + var length = queue.length; + for(var i=0;i
'); - } + }; }; /** - * @ngdoc overview + * @ngdoc module * @name ngMock * @description * - * The `ngMock` is an angular module which is used with `ng` module and adds unit-test configuration as well as useful - * mocks to the {@link AUTO.$injector $injector}. + * # ngMock + * + * The `ngMock` module provides support to inject and mock Angular services into unit tests. + * In addition, ngMock also extends various core ng services such that they can be + * inspected and controlled in a synchronous manner within test code. + * + * + *
+ * */ angular.module('ngMock', ['ng']).provider({ $browser: angular.mock.$BrowserProvider, $exceptionHandler: angular.mock.$ExceptionHandlerProvider, $log: angular.mock.$LogProvider, + $interval: angular.mock.$IntervalProvider, $httpBackend: angular.mock.$HttpBackendProvider, $rootElement: angular.mock.$RootElementProvider -}).config(function($provide) { +}).config(['$provide', function($provide) { $provide.decorator('$timeout', angular.mock.$TimeoutDecorator); -}); + $provide.decorator('$$rAF', angular.mock.$RAFDecorator); + $provide.decorator('$$asyncCallback', angular.mock.$AsyncCallbackDecorator); +}]); /** - * @ngdoc overview + * @ngdoc module * @name ngMockE2E + * @module ngMockE2E * @description * * The `ngMockE2E` is an angular module which contains mocks suitable for end-to-end testing. * Currently there is only one mock present in this module - * the {@link ngMockE2E.$httpBackend e2e $httpBackend} mock. */ -angular.module('ngMockE2E', ['ng']).config(function($provide) { +angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator); -}); +}]); /** - * @ngdoc object - * @name ngMockE2E.$httpBackend + * @ngdoc service + * @name $httpBackend + * @module ngMockE2E * @description * Fake HTTP backend implementation suitable for end-to-end testing or backend-less development of * applications that use the {@link ng.$http $http service}. @@ -1562,7 +1790,7 @@ angular.module('ngMockE2E', ['ng']).config(function($provide) { * To setup the application to run with this http backend, you have to create a module that depends * on the `ngMockE2E` and your application modules and defines the fake backend: * - *
+ * ```js
  *   myAppDev = angular.module('myAppDev', ['myApp', 'ngMockE2E']);
  *   myAppDev.run(function($httpBackend) {
  *     phones = [{name: 'phone1'}, {name: 'phone2'}];
@@ -1572,20 +1800,22 @@ angular.module('ngMockE2E', ['ng']).config(function($provide) {
  *
  *     // adds a new phone to the phones array
  *     $httpBackend.whenPOST('/phones').respond(function(method, url, data) {
- *       phones.push(angular.fromJSON(data));
+ *       var phone = angular.fromJson(data);
+ *       phones.push(phone);
+ *       return [200, phone, {}];
  *     });
  *     $httpBackend.whenGET(/^\/templates\//).passThrough();
  *     //...
  *   });
- * 
+ * ``` * * Afterwards, bootstrap your app with this new module. */ /** * @ngdoc method - * @name ngMockE2E.$httpBackend#when - * @methodOf ngMockE2E.$httpBackend + * @name $httpBackend#when + * @module ngMockE2E * @description * Creates a new backend definition. * @@ -1597,19 +1827,21 @@ angular.module('ngMockE2E', ['ng']).config(function($provide) { * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that * control how a matched request is handled. * - * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` + * - respond – + * `{function([status,] data[, headers, statusText]) + * | function(function(method, url, data, headers)}` * – The respond method takes a set of static data to be returned or a function that can return - * an array containing response status (number), response data (string) and response headers - * (Object). - * - passThrough – `{function()}` – Any request matching a backend definition with `passThrough` - * handler, will be pass through to the real backend (an XHR request will be made to the - * server. + * an array containing response status (number), response data (string), response headers + * (Object), and the text for the status (string). + * - passThrough – `{function()}` – Any request matching a backend definition with + * `passThrough` handler will be passed through to the real backend (an XHR request will be made + * to the server.) */ /** * @ngdoc method - * @name ngMockE2E.$httpBackend#whenGET - * @methodOf ngMockE2E.$httpBackend + * @name $httpBackend#whenGET + * @module ngMockE2E * @description * Creates a new backend definition for GET requests. For more info see `when()`. * @@ -1621,8 +1853,8 @@ angular.module('ngMockE2E', ['ng']).config(function($provide) { /** * @ngdoc method - * @name ngMockE2E.$httpBackend#whenHEAD - * @methodOf ngMockE2E.$httpBackend + * @name $httpBackend#whenHEAD + * @module ngMockE2E * @description * Creates a new backend definition for HEAD requests. For more info see `when()`. * @@ -1634,8 +1866,8 @@ angular.module('ngMockE2E', ['ng']).config(function($provide) { /** * @ngdoc method - * @name ngMockE2E.$httpBackend#whenDELETE - * @methodOf ngMockE2E.$httpBackend + * @name $httpBackend#whenDELETE + * @module ngMockE2E * @description * Creates a new backend definition for DELETE requests. For more info see `when()`. * @@ -1647,8 +1879,8 @@ angular.module('ngMockE2E', ['ng']).config(function($provide) { /** * @ngdoc method - * @name ngMockE2E.$httpBackend#whenPOST - * @methodOf ngMockE2E.$httpBackend + * @name $httpBackend#whenPOST + * @module ngMockE2E * @description * Creates a new backend definition for POST requests. For more info see `when()`. * @@ -1661,8 +1893,8 @@ angular.module('ngMockE2E', ['ng']).config(function($provide) { /** * @ngdoc method - * @name ngMockE2E.$httpBackend#whenPUT - * @methodOf ngMockE2E.$httpBackend + * @name $httpBackend#whenPUT + * @module ngMockE2E * @description * Creates a new backend definition for PUT requests. For more info see `when()`. * @@ -1675,8 +1907,8 @@ angular.module('ngMockE2E', ['ng']).config(function($provide) { /** * @ngdoc method - * @name ngMockE2E.$httpBackend#whenPATCH - * @methodOf ngMockE2E.$httpBackend + * @name $httpBackend#whenPATCH + * @module ngMockE2E * @description * Creates a new backend definition for PATCH requests. For more info see `when()`. * @@ -1689,8 +1921,8 @@ angular.module('ngMockE2E', ['ng']).config(function($provide) { /** * @ngdoc method - * @name ngMockE2E.$httpBackend#whenJSONP - * @methodOf ngMockE2E.$httpBackend + * @name $httpBackend#whenJSONP + * @module ngMockE2E * @description * Creates a new backend definition for JSONP requests. For more info see `when()`. * @@ -1699,7 +1931,8 @@ angular.module('ngMockE2E', ['ng']).config(function($provide) { * control how a matched request is handled. */ angular.mock.e2e = {}; -angular.mock.e2e.$httpBackendDecorator = ['$rootScope', '$delegate', '$browser', createHttpBackendMock]; +angular.mock.e2e.$httpBackendDecorator = + ['$rootScope', '$delegate', '$browser', createHttpBackendMock]; angular.mock.clearDataCache = function() { @@ -1707,50 +1940,43 @@ angular.mock.clearDataCache = function() { cache = angular.element.cache; for(key in cache) { - if (cache.hasOwnProperty(key)) { + if (Object.prototype.hasOwnProperty.call(cache,key)) { var handle = cache[key].handle; - handle && angular.element(handle.elem).unbind(); + handle && angular.element(handle.elem).off(); delete cache[key]; } } }; -window.jstestdriver && (function(window) { - /** - * Global method to output any number of objects into JSTD console. Useful for debugging. - */ - window.dump = function() { - var args = []; - angular.forEach(arguments, function(arg) { - args.push(angular.mock.dump(arg)); - }); - jstestdriver.console.log.apply(jstestdriver.console, args); - if (window.console) { - window.console.log.apply(window.console, args); - } - }; -})(window); +if(window.jasmine || window.mocha) { + var currentSpec = null, + isSpecRunning = function() { + return !!currentSpec; + }; -(window.jasmine || window.mocha) && (function(window) { - - var currentSpec = null; - beforeEach(function() { + (window.beforeEach || window.setup)(function() { currentSpec = this; }); - afterEach(function() { + (window.afterEach || window.teardown)(function() { var injector = currentSpec.$injector; + angular.forEach(currentSpec.$modules, function(module) { + if (module && module.$$hashKey) { + module.$$hashKey = undefined; + } + }); + currentSpec.$injector = null; currentSpec.$modules = null; currentSpec = null; if (injector) { - injector.get('$rootElement').unbind(); + injector.get('$rootElement').off(); injector.get('$browser').pollFns.length = 0; } @@ -1769,10 +1995,6 @@ window.jstestdriver && (function(window) { angular.callbacks.counter = 0; }); - function isSpecRunning() { - return currentSpec && (window.mocha || currentSpec.queue.running); - } - /** * @ngdoc function * @name angular.mock.module @@ -1785,9 +2007,11 @@ window.jstestdriver && (function(window) { * * See {@link angular.mock.inject inject} for usage example * - * @param {...(string|Function)} fns any number of modules which are represented as string + * @param {...(string|Function|Object)} fns any number of modules which are represented as string * aliases or as anonymous module initialization functions. The modules are used to - * configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. + * configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. If an + * object literal is passed they will be registered as values in the module, the key being + * the module name and the value being what is returned. */ window.module = angular.mock.module = function() { var moduleFns = Array.prototype.slice.call(arguments, 0); @@ -1795,11 +2019,19 @@ window.jstestdriver && (function(window) { ///////////////////// function workFn() { if (currentSpec.$injector) { - throw Error('Injector already created, can not register a module!'); + throw new Error('Injector already created, can not register a module!'); } else { var modules = currentSpec.$modules || (currentSpec.$modules = []); angular.forEach(moduleFns, function(module) { - modules.push(module); + if (angular.isObject(module) && !angular.isArray(module)) { + modules.push(function($provide) { + angular.forEach(module, function(value, key) { + $provide.value(key, value); + }); + }); + } else { + modules.push(module); + } }); } } @@ -1813,13 +2045,45 @@ window.jstestdriver && (function(window) { * *NOTE*: This function is also published on window for easy access.
* * The inject function wraps a function into an injectable function. The inject() creates new - * instance of {@link AUTO.$injector $injector} per test, which is then used for + * instance of {@link auto.$injector $injector} per test, which is then used for * resolving references. * - * See also {@link angular.mock.module module} * + * ## Resolving References (Underscore Wrapping) + * Often, we would like to inject a reference once, in a `beforeEach()` block and reuse this + * in multiple `it()` clauses. To be able to do this we must assign the reference to a variable + * that is declared in the scope of the `describe()` block. Since we would, most likely, want + * the variable to have the same name of the reference we have a problem, since the parameter + * to the `inject()` function would hide the outer variable. + * + * To help with this, the injected parameters can, optionally, be enclosed with underscores. + * These are ignored by the injector when the reference name is resolved. + * + * For example, the parameter `_myService_` would be resolved as the reference `myService`. + * Since it is available in the function body as _myService_, we can then assign it to a variable + * defined in an outer scope. + * + * ``` + * // Defined out reference variable outside + * var myService; + * + * // Wrap the parameter in underscores + * beforeEach( inject( function(_myService_){ + * myService = _myService_; + * })); + * + * // Use myService in a series of tests. + * it('makes use of myService', function() { + * myService.doStuff(); + * }); + * + * ``` + * + * See also {@link angular.mock.module angular.mock.module} + * + * ## Example * Example of what a typical jasmine tests looks like with the inject method. - *
+   * ```js
    *
    *   angular.module('myApplicationModule', [])
    *       .value('mode', 'app')
@@ -1850,17 +2114,31 @@ window.jstestdriver && (function(window) {
    *       inject(function(version) {
    *         expect(version).toEqual('overridden');
    *       });
-   *     ));
+   *     });
    *   });
    *
-   * 
+ * ``` * * @param {...Function} fns any number of functions which will be injected using the injector. */ + + + + var ErrorAddingDeclarationLocationStack = function(e, errorForStack) { + this.message = e.message; + this.name = e.name; + if (e.line) this.line = e.line; + if (e.sourceId) this.sourceId = e.sourceId; + if (e.stack && errorForStack) + this.stack = e.stack + '\n' + errorForStack.stack; + if (e.stackArray) this.stackArray = e.stackArray; + }; + ErrorAddingDeclarationLocationStack.prototype.toString = Error.prototype.toString; + window.inject = angular.mock.inject = function() { var blockFns = Array.prototype.slice.call(arguments, 0); var errorForStack = new Error('Declaration Location'); - return isSpecRunning() ? workFn() : workFn; + return isSpecRunning() ? workFn.call(currentSpec) : workFn; ///////////////////// function workFn() { var modules = currentSpec.$modules || []; @@ -1873,9 +2151,13 @@ window.jstestdriver && (function(window) { } for(var i = 0, ii = blockFns.length; i < ii; i++) { try { + /* jshint -W040 *//* Jasmine explicitly provides a `this` object when calling functions */ injector.invoke(blockFns[i] || angular.noop, this); + /* jshint +W040 */ } catch (e) { - if(e.stack && errorForStack) e.stack += '\n' + errorForStack.stack; + if (e.stack && errorForStack) { + throw new ErrorAddingDeclarationLocationStack(e, errorForStack); + } throw e; } finally { errorForStack = null; @@ -1883,4 +2165,7 @@ window.jstestdriver && (function(window) { } } }; -})(window); \ No newline at end of file +} + + +})(window, window.angular);