diff --git a/.gitignore b/.gitignore index fa152b4..51a9a51 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ logs/* build/ .DS_Store /node_modules/ +*.log +.grunt diff --git a/.travis.yml b/.travis.yml index 758db5b..481a518 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,8 +5,10 @@ node_js: notifications: email: true -before_script: +install: + - npm install - npm install -g grunt-cli - + - npm install bower + - bower install script: - grunt tests diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4fbf9af --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,75 @@ +##Change Log + +###Version 1.3.1 +* Fix broken bower distribution + +###Version 1.3.0 +* MomentJS integration - https://github.com/siddii/angular-timer/pull/159 + +###Version 1.2.1 +* Add adaptive interpolation symbol - https://github.com/siddii/angular-timer/pull/153 + +###Version 1.2.0 +* Reset method https://github.com/siddii/angular-timer/pull/46 + +###Version 1.1.9 +* Fix for issue https://github.com/siddii/angular-timer/issues/128 (Remove class scoping on `timer` directive) + +###Version 1.1.8 +* Fix for issue https://github.com/siddii/angular-timer/issues/117 (0 minute display 0 minutes) + +###Version 1.1.7 +* Fix for https://github.com/siddii/angular-timer/issues/101 (start-time doesn't work in combination with autostart) + +###Version 1.1.6 +* Added countdown finished callback - https://github.com/siddii/angular-timer/pull/64 + +###Version 1.1.5 +* Fix for - https://github.com/siddii/angular-timer/issues/84 + +###Version 1.1.4 +* CommonJS Compliance - https://github.com/siddii/angular-timer/pull/80 + +###Version 1.1.3 +* Setting `countdownattr` value on `tick` - https://github.com/siddii/angular-timer/pull/78 + +###Version 1.1.2 +* Countdown time max-time-unit for month & year - https://github.com/siddii/angular-timer/pull/74 + +###Version 1.1.1 +* Fix for https://github.com/siddii/angular-timer/issues/73 + +###Version 1.1.0 +* Plural/singular units. PR - https://github.com/siddii/angular-timer/pull/54 +* IE8 support. PR - https://github.com/siddii/angular-timer/pull/62 + +###Version 1.0.12 +* `clear` method. Please see this PR - https://github.com/siddii/angular-timer/pull/43 + +###Version 1.0.11 +* `addCDSeconds` method for running countdown timer + +###Version 1.0.10 +* Minor bug fixes - https://github.com/siddii/angular-timer/issues/33 + +###Version 1.0.9 +* `end-time` attribute + +###Version 1.0.8 +* Fixing the broken stuff in the last version + +###Version 1.0.7 +* Ignore `bower_components` folder - https://github.com/siddii/angular-timer/issues/30 +* Templates doesn't work with angular 1.2.0 - https://github.com/siddii/angular-timer/issues/29 + +###Version 1.0.6 +* Removing `node_modules` folder. Fix for - https://github.com/siddii/angular-timer/issues/26 + +###Version 1.0.5 +* Performance Optimization. Please see - https://github.com/siddii/angular-timer/pull/25 + +###Version 1.0.4 +* 'auto-start' attribute name change to 'autostart' in support of Angular 1.2. See #14 + +###Version 1.0.3 +* Successful Bower integration! \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js index 3c3b5db..7215355 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -8,6 +8,7 @@ module.exports = function (grunt) { grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-karma'); grunt.loadNpmTasks('grunt-contrib-connect'); + grunt.loadNpmTasks('grunt-gh-pages'); var userConfig = { dist_dir: 'dist', @@ -30,15 +31,66 @@ module.exports = function (grunt) { ' */\n' }, + /** + * The directories to delete when `grunt clean` is executed. + */ + clean: [ + '<%= dist_dir %>' + ], + + /* Copy all example into dist/examples */ + copy: { + examples: { + src: 'examples/*', + dest: 'dist/' + }, + nav: { + src: 'navbar.html', + dest: 'dist/' + }, + example: { + src: 'examples.html', + dest: 'dist/' + } + }, + concat: { compile_js: { options: { banner: '<%= meta.banner %>' }, src: [ - 'app/**/*.js' + 'app/**/*.js' ], dest: '<%= dist_dir %>/<%= pkg.name %>.js' + }, + compile_all_js: { + src: [ + '<%= dist_dir %>/<%= pkg.name %>.min.js', + 'bower_components/moment/min/moment-with-locales.min.js', + 'bower_components/humanize-duration/humanize-duration.js' + ], + dest: '<%= dist_dir %>/assets/js/<%= pkg.name %>-all.min.js' + }, + compile_bower_js: { + src: [ + 'bower_components/angular/angular.min.js', + 'bower_components/jquery/jquery.min.js', + 'bower_components/bootstrap/docs/assets/js/bootstrap.min.js', + 'docs/docs.js', + 'docs/prettify.js', + 'docs/application.js' + ], + dest: '<%= dist_dir %>/assets/js/<%= pkg.name %>-bower.js' + }, + compile_bower_css: { + src: [ + 'bower_components/bootstrap/docs/assets/css/bootstrap.css', + 'bower_components/bootstrap/docs/assets/css/bootstrap-responsive.css', + 'docs/css/docs.css', + 'docs/css/prettify.css' + ], + dest: '<%= dist_dir %>/assets/css/<%= pkg.name %>-bower.css' } }, @@ -67,7 +119,15 @@ module.exports = function (grunt) { sub: true, boss: true, eqnull: true + } + }, + + 'gh-pages': { + options: { + base: 'dist', + message: 'Update gh-pages' }, + src: ['**'] }, connect: { @@ -119,7 +179,52 @@ module.exports = function (grunt) { grunt.registerTask('tests', [ 'connect:testserver', 'build', 'karma:unit', 'karma:e2e']); grunt.registerTask('build', [ - 'jshint', 'concat', 'uglify' + 'clean', 'jshint', 'concat:compile_js', 'uglify', 'concat:compile_all_js', 'concat:compile_bower_js', 'concat:compile_bower_css','copy:examples','copy:nav','copy:example' ]); + /** + * A utility function to get all app JavaScript sources. + */ + function filterForJS ( files ) { + return files.filter( function ( file ) { + return file.match( /\.js$/ ); + }); + } + + /** + * A utility function to get all app CSS sources. + */ + function filterForCSS ( files ) { + return files.filter( function ( file ) { + return file.match( /\.css$/ ); + }); + } + + /** + * The index.html template includes the stylesheet and javascript sources + * based on dynamic names calculated in this Gruntfile. This task assembles + * the list into variables for the template to use and then runs the + * compilation. + */ + grunt.registerMultiTask( 'index', 'Process index.html template', function () { + var dirRE = new RegExp( '^('+grunt.config('build_dir')+'|'+grunt.config('dist_dir')+')\/', 'g' ); + var jsFiles = filterForJS( this.filesSrc ).map( function ( file ) { + return file.replace( dirRE, '' ); + }); + var cssFiles = filterForCSS( this.filesSrc ).map( function ( file ) { + return file.replace( dirRE, '' ); + }); + + grunt.file.copy('index.tpl.html', this.data.dir + 'index.html', { + process: function ( contents, path ) { + return grunt.template.process( contents, { + data: { + scripts: jsFiles, + styles: cssFiles, + version: grunt.config( 'pkg.version' ) + } + }); + } + }); + }); }; diff --git a/README.md b/README.md index 90d2ac8..83f19f6 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,28 @@ -## angular-timer — A simple, re-usable, inter-operable timer directive [](https://travis-ci.org/siddii/angular-timer) +## angular-timer — A simple, re-usable, inter-operable timer directive + +[](https://travis-ci.org/siddii/angular-timer) +[](https://www.codeshelter.co/) ### Getting started With any of the following options... * Download the latest script file - https://raw.github.com/siddii/angular-timer/master/dist/angular-timer.min.js * Clone the repo - `git@github.com:siddii/angular-timer.git` * Install angular-timer using [Bower](http://bower.io) - `bower install angular-timer` +* Add ``timer`` to your list of modules + +### Requirements +With Bower install : +* Install humanize-duration using [Bower](http://bower.io) - `bower install humanize-duration` +* Install momentjs using [Bower](http://bower.io) - `bower install momentjs` + +And include these scripts in your webpage : +* bower_components/momentjs/min/moment.min.js +* bower_components/momentjs/min/locales.min.js +* bower_components/humanize-duration/humanize-duration.js + ### Running locally +Install all bower components - `bower install` Using [Grunt](http://gruntjs.com/) type `grunt` from command line, the default task will open index.html page in your default browser @@ -16,4 +32,5 @@ Following command will run both unit & End-to-End (e2e) tests grunt tests ``` - +### Examples +There are some examples on the index page http://siddii.github.io/angular-timer/index.html. Please go over them to get an understanding on how this module works. diff --git a/app/js/_timer.js b/app/js/_timer.js new file mode 100644 index 0000000..70ca899 --- /dev/null +++ b/app/js/_timer.js @@ -0,0 +1,425 @@ +var timerModule = angular.module('timer', []) + .directive('timer', ['$compile', function ($compile) { + return { + restrict: 'EA', + replace: false, + scope: { + interval: '=interval', + startTimeAttr: '=startTime', + endTimeAttr: '=endTime', + countdownattr: '=countdown', + finishCallback: '&finishCallback', + autoStart: '&autoStart', + language: '@?', + fallback: '@?', + maxTimeUnit: '=', + seconds: '=?', + minutes: '=?', + hours: '=?', + days: '=?', + months: '=?', + years: '=?', + secondsS: '=?', + minutesS: '=?', + hoursS: '=?', + daysS: '=?', + monthsS: '=?', + yearsS: '=?' + }, + controller: ['$scope', '$element', '$attrs', '$timeout', 'I18nService', '$interpolate', 'progressBarService', function ($scope, $element, $attrs, $timeout, I18nService, $interpolate, progressBarService) { + + // Checking for trim function since IE8 doesn't have it + // If not a function, create tirm with RegEx to mimic native trim + if (typeof String.prototype.trim !== 'function') { + String.prototype.trim = function () { + return this.replace(/^\s+|\s+$/g, ''); + }; + } + + //angular 1.2 doesn't support attributes ending in "-start", so we're + //supporting both "autostart" and "auto-start" as a solution for + //backward and forward compatibility. + $scope.autoStart = $attrs.autoStart || $attrs.autostart; + + + $scope.language = $scope.language || 'en'; + $scope.fallback = $scope.fallback || 'en'; + + //allow to change the language of the directive while already launched + $scope.$watch('language', function(newVal, oldVal) { + if(newVal !== undefined) { + i18nService.init(newVal, $scope.fallback); + } + }); + + //init momentJS i18n, default english + var i18nService = new I18nService(); + i18nService.init($scope.language, $scope.fallback); + + //progress bar + $scope.displayProgressBar = 0; + $scope.displayProgressActive = 'active'; //Bootstrap active effect for progress bar + + if ($element.html().trim().length === 0) { + $element.append($compile('' + $interpolate.startSymbol() + 'millis' + $interpolate.endSymbol() + '')($scope)); + } else { + $element.append($compile($element.contents())($scope)); + } + + $scope.startTime = null; + $scope.endTime = null; + $scope.timeoutId = null; + $scope.countdown = angular.isNumber($scope.countdownattr) && parseInt($scope.countdownattr, 10) >= 0 ? parseInt($scope.countdownattr, 10) : undefined; + $scope.isRunning = false; + + $scope.$on('timer-start', function () { + $scope.start(); + }); + + $scope.$on('timer-resume', function () { + $scope.resume(); + }); + + $scope.$on('timer-stop', function () { + $scope.stop(); + }); + + $scope.$on('timer-clear', function () { + $scope.clear(); + }); + + $scope.$on('timer-reset', function () { + $scope.reset(); + }); + + $scope.$on('timer-set-countdown', function (e, countdown) { + $scope.countdown = countdown; + }); + + function resetTimeout() { + if ($scope.timeoutId) { + clearTimeout($scope.timeoutId); + } + } + + $scope.$watch('startTimeAttr', function(newValue, oldValue) { + if (newValue !== oldValue && $scope.isRunning) { + $scope.start(); + } + }); + + $scope.$watch('endTimeAttr', function(newValue, oldValue) { + if (newValue !== oldValue && $scope.isRunning) { + $scope.start(); + } + }); + + $scope.start = function () { + $scope.startTime = $scope.startTimeAttr ? moment($scope.startTimeAttr) : moment(); + $scope.endTime = $scope.endTimeAttr ? moment($scope.endTimeAttr) : null; + if (!angular.isNumber($scope.countdown)) { + $scope.countdown = angular.isNumber($scope.countdownattr) && parseInt($scope.countdownattr, 10) > 0 ? parseInt($scope.countdownattr, 10) : undefined; + } + resetTimeout(); + tick(); + $scope.isRunning = true; + $scope.$emit('timer-started', { + timeoutId: $scope.timeoutId, + millis: $scope.millis, + seconds: $scope.seconds, + minutes: $scope.minutes, + hours: $scope.hours, + days: $scope.days + }); + }; + + $scope.resume = function () { + resetTimeout(); + if ($scope.countdownattr) { + $scope.countdown += 1; + } + $scope.startTime = moment().diff((moment($scope.stoppedTime).diff(moment($scope.startTime)))); + tick(); + $scope.isRunning = true; + $scope.$emit('timer-started', { + timeoutId: $scope.timeoutId, + millis: $scope.millis, + seconds: $scope.seconds, + minutes: $scope.minutes, + hours: $scope.hours, + days: $scope.days + }); + }; + + $scope.stop = $scope.pause = function () { + var timeoutId = $scope.timeoutId; + $scope.clear(); + $scope.$emit('timer-stopped', { + timeoutId: timeoutId, + millis: $scope.millis, + seconds: $scope.seconds, + minutes: $scope.minutes, + hours: $scope.hours, + days: $scope.days + }); + }; + + $scope.clear = function () { + // same as stop but without the event being triggered + $scope.stoppedTime = moment(); + resetTimeout(); + $scope.timeoutId = null; + $scope.isRunning = false; + }; + + $scope.reset = function () { + $scope.startTime = $scope.startTimeAttr ? moment($scope.startTimeAttr) : moment(); + $scope.endTime = $scope.endTimeAttr ? moment($scope.endTimeAttr) : null; + $scope.countdown = angular.isNumber($scope.countdownattr) && parseInt($scope.countdownattr, 10) > 0 ? parseInt($scope.countdownattr, 10) : undefined; + resetTimeout(); + tick(); + $scope.isRunning = false; + $scope.clear(); + $scope.$emit('timer-reseted', { + timeoutId: $scope.timeoutId, + millis: $scope.millis, + seconds: $scope.seconds, + minutes: $scope.minutes, + hours: $scope.hours, + days: $scope.days + }); + }; + + $element.bind('$destroy', function () { + resetTimeout(); + $scope.isRunning = false; + }); + + + function calculateTimeUnits() { + var timeUnits = {}; //will contains time with units + + if ($attrs.startTime !== undefined){ + $scope.millis = moment().diff(moment($scope.startTimeAttr)); + } + + timeUnits = i18nService.getTimeUnits($scope.millis); + + // compute time values based on maxTimeUnit specification + if (!$scope.maxTimeUnit || $scope.maxTimeUnit === 'day') { + $scope.seconds = Math.floor(($scope.millis / 1000) % 60); + $scope.minutes = Math.floor((($scope.millis / (60000)) % 60)); + $scope.hours = Math.floor((($scope.millis / (3600000)) % 24)); + $scope.days = Math.floor((($scope.millis / (3600000)) / 24)); + $scope.months = 0; + $scope.years = 0; + } else if ($scope.maxTimeUnit === 'second') { + $scope.seconds = Math.floor($scope.millis / 1000); + $scope.minutes = 0; + $scope.hours = 0; + $scope.days = 0; + $scope.months = 0; + $scope.years = 0; + } else if ($scope.maxTimeUnit === 'minute') { + $scope.seconds = Math.floor(($scope.millis / 1000) % 60); + $scope.minutes = Math.floor($scope.millis / 60000); + $scope.hours = 0; + $scope.days = 0; + $scope.months = 0; + $scope.years = 0; + } else if ($scope.maxTimeUnit === 'hour') { + $scope.seconds = Math.floor(($scope.millis / 1000) % 60); + $scope.minutes = Math.floor((($scope.millis / (60000)) % 60)); + $scope.hours = Math.floor($scope.millis / 3600000); + $scope.days = 0; + $scope.months = 0; + $scope.years = 0; + } else if ($scope.maxTimeUnit === 'month') { + $scope.seconds = Math.floor(($scope.millis / 1000) % 60); + $scope.minutes = Math.floor((($scope.millis / (60000)) % 60)); + $scope.hours = Math.floor((($scope.millis / (3600000)) % 24)); + $scope.days = Math.floor((($scope.millis / (3600000)) / 24) % 30); + $scope.months = Math.floor((($scope.millis / (3600000)) / 24) / 30); + $scope.years = 0; + } else if ($scope.maxTimeUnit === 'year') { + $scope.seconds = Math.floor(($scope.millis / 1000) % 60); + $scope.minutes = Math.floor((($scope.millis / (60000)) % 60)); + $scope.hours = Math.floor((($scope.millis / (3600000)) % 24)); + $scope.days = Math.floor((($scope.millis / (3600000)) / 24) % 30); + $scope.months = Math.floor((($scope.millis / (3600000)) / 24 / 30) % 12); + $scope.years = Math.floor(($scope.millis / (3600000)) / 24 / 365); + } + // plural - singular unit decision (old syntax, for backwards compatibility and English only, could be deprecated!) + $scope.secondsS = ($scope.seconds === 1) ? '' : 's'; + $scope.minutesS = ($scope.minutes === 1) ? '' : 's'; + $scope.hoursS = ($scope.hours === 1) ? '' : 's'; + $scope.daysS = ($scope.days === 1)? '' : 's'; + $scope.monthsS = ($scope.months === 1)? '' : 's'; + $scope.yearsS = ($scope.years === 1)? '' : 's'; + + + // new plural-singular unit decision functions (for custom units and multilingual support) + $scope.secondUnit = timeUnits.seconds; + $scope.minuteUnit = timeUnits.minutes; + $scope.hourUnit = timeUnits.hours; + $scope.dayUnit = timeUnits.days; + $scope.monthUnit = timeUnits.months; + $scope.yearUnit = timeUnits.years; + + //add leading zero if number is smaller than 10 + $scope.sseconds = $scope.seconds < 10 ? '0' + $scope.seconds : $scope.seconds; + $scope.mminutes = $scope.minutes < 10 ? '0' + $scope.minutes : $scope.minutes; + $scope.hhours = $scope.hours < 10 ? '0' + $scope.hours : $scope.hours; + $scope.ddays = $scope.days < 10 ? '0' + $scope.days : $scope.days; + $scope.mmonths = $scope.months < 10 ? '0' + $scope.months : $scope.months; + $scope.yyears = $scope.years < 10 ? '0' + $scope.years : $scope.years; + + } + + //determine initial values of time units and add AddSeconds functionality + if ($scope.countdownattr) { + $scope.millis = $scope.countdownattr * 1000; + + $scope.addCDSeconds = function (extraSeconds) { + $scope.countdown += extraSeconds; + if (!$scope.isRunning) { + $scope.start(); + } + }; + + $scope.$on('timer-add-cd-seconds', function (e, extraSeconds) { + $scope.addCDSeconds(extraSeconds); + }); + + $scope.$on('timer-set-countdown-seconds', function (e, countdownSeconds) { + if (!$scope.isRunning) { + $scope.clear(); + } + + $scope.countdown = countdownSeconds; + $scope.millis = countdownSeconds * 1000; + calculateTimeUnits(); + }); + } else { + $scope.millis = 0; + } + calculateTimeUnits(); + + var tick = function tick() { + var typeTimer = null; // countdown or endTimeAttr + $scope.millis = moment().diff($scope.startTime); + var adjustment = $scope.millis % 1000; + + if ($scope.endTimeAttr) { + typeTimer = $scope.endTimeAttr; + $scope.millis = moment($scope.endTime).diff(moment()); + adjustment = $scope.interval - $scope.millis % 1000; + } + + if ($scope.countdownattr) { + typeTimer = $scope.countdownattr; + $scope.millis = $scope.countdown * 1000; + } + + if ($scope.millis < 0) { + $scope.stop(); + $scope.millis = 0; + calculateTimeUnits(); + if($scope.finishCallback) { + $scope.$eval($scope.finishCallback); + } + return; + } + calculateTimeUnits(); + + //We are not using $timeout for a reason. Please read here - https://github.com/siddii/angular-timer/pull/5 + $scope.timeoutId = setTimeout(function () { + tick(); + // since you choose not to use $timeout, at least preserve angular cycle two way data binding + // by calling $scope.$apply() instead of $scope.$digest() + $scope.$apply(); + }, $scope.interval - adjustment); + + $scope.$emit('timer-tick', { + timeoutId: $scope.timeoutId, + millis: $scope.millis, + seconds: $scope.seconds, + minutes: $scope.minutes, + hours: $scope.hours, + days: $scope.days + }); + + if ($scope.countdown > 0) { + $scope.countdown--; + } + else if ($scope.countdown <= 0) { + $scope.stop(); + if($scope.finishCallback) { + $scope.$eval($scope.finishCallback); + } + } + + if(typeTimer !== null){ + //calculate progress bar + $scope.progressBar = progressBarService.calculateProgressBar($scope.startTime, $scope.millis, $scope.endTime, $scope.countdownattr); + + if($scope.progressBar === 100){ + $scope.displayProgressActive = ''; //No more Bootstrap active effect + } + } + }; + + if ($scope.autoStart === undefined || $scope.autoStart === true) { + $scope.start(); + } + }] + }; + }]) + .directive('timerControls', function() { + return { + restrict: 'EA', + scope: true, + controller: ['$scope', function($scope) { + $scope.timerStatus = "reset"; + $scope.$on('timer-started', function() { + $scope.timerStatus = "started"; + }); + $scope.$on('timer-stopped', function() { + $scope.timerStatus = "stopped"; + }); + $scope.$on('timer-reset', function() { + $scope.timerStatus = "reset"; + }); + $scope.timerStart = function() { + $scope.$broadcast('timer-start'); + }; + $scope.timerStop = function() { + $scope.$broadcast('timer-stop'); + }; + $scope.timerResume = function() { + $scope.$broadcast('timer-resume'); + }; + $scope.timerToggle = function() { + switch ($scope.timerStatus) { + case "started": + $scope.timerStop(); + break; + case "stopped": + $scope.timerResume(); + break; + case "reset": + $scope.timerStart(); + break; + } + }; + $scope.timerAddCDSeconds = function(extraSeconds) { + $scope.$broadcast('timer-add-cd-seconds', extraSeconds); + }; + }] + }; + }); + +/* commonjs package manager support (eg componentjs) */ +if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports){ + module.exports = timerModule; +} diff --git a/app/js/i18nService.js b/app/js/i18nService.js new file mode 100644 index 0000000..9f7a596 --- /dev/null +++ b/app/js/i18nService.js @@ -0,0 +1,65 @@ +var app = angular.module('timer'); + +app.factory('I18nService', function() { + + var I18nService = function() {}; + + I18nService.prototype.language = 'en'; + I18nService.prototype.fallback = 'en'; + I18nService.prototype.timeHumanizer = {}; + + I18nService.prototype.init = function init(lang, fallback) { + var supported_languages = humanizeDuration.getSupportedLanguages(); + + this.fallback = (fallback !== undefined) ? fallback : 'en'; + if (supported_languages.indexOf(fallback) === -1) { + this.fallback = 'en'; + } + + this.language = lang; + if (supported_languages.indexOf(lang) === -1) { + this.language = this.fallback; + } + + // It should be handle by the user's application itself, and not inside the directive + // moment init + // moment.locale(this.language); + + //human duration init, using it because momentjs does not allow accurate time ( + // momentJS: a few moment ago, human duration : 4 seconds ago + this.timeHumanizer = humanizeDuration.humanizer({ + language: this.language, + halfUnit:false + }); + }; + + /** + * get time with units from momentJS i18n + * @param {int} millis + * @returns {{millis: string, seconds: string, minutes: string, hours: string, days: string, months: string, years: string}} + */ + I18nService.prototype.getTimeUnits = function getTimeUnits(millis) { + var diffFromAlarm = Math.round(millis/1000) * 1000; //time in milliseconds, get rid of the last 3 ms value to avoid 2.12 seconds display + + var time = {}; + + if (typeof this.timeHumanizer != 'undefined'){ + time = { + 'millis' : this.timeHumanizer(diffFromAlarm, { units: ["ms"] }), + 'seconds' : this.timeHumanizer(diffFromAlarm, { units: ["s"] }), + 'minutes' : this.timeHumanizer(diffFromAlarm, { units: ["m", "s"] }) , + 'hours' : this.timeHumanizer(diffFromAlarm, { units: ["h", "m", "s"] }) , + 'days' : this.timeHumanizer(diffFromAlarm, { units: ["d", "h", "m", "s"] }) , + 'months' : this.timeHumanizer(diffFromAlarm, { units: ["mo", "d", "h", "m", "s"] }) , + 'years' : this.timeHumanizer(diffFromAlarm, { units: ["y", "mo", "d", "h", "m", "s"] }) + }; + } + else { + console.error('i18nService has not been initialized. You must call i18nService.init("en") for example'); + } + + return time; + }; + + return I18nService; +}); diff --git a/app/js/progressBarService.js b/app/js/progressBarService.js new file mode 100644 index 0000000..b6eb87c --- /dev/null +++ b/app/js/progressBarService.js @@ -0,0 +1,46 @@ +var app = angular.module('timer'); + +app.factory('progressBarService', function() { + + var ProgressBarService = function() {}; + + /** + * calculate the remaining time in a progress bar in percentage + * @param {momentjs} startValue in seconds + * @param {integer} currentCountdown, where are we in the countdown + * @param {integer} remainingTime, remaining milliseconds + * @param {integer} endTime, end time, can be undefined + * @param {integer} coutdown, original coutdown value, can be undefined + * + * joke : https://www.youtube.com/watch?v=gENVB6tjq_M + * @return {float} 0 --> 100 + */ + ProgressBarService.prototype.calculateProgressBar = function calculateProgressBar(startValue, remainingTime, endTimeAttr, coutdown) { + var displayProgressBar = 0, + endTimeValue, + initialCountdown; + + remainingTime = remainingTime / 1000; //seconds + + + if(endTimeAttr !== null){ + endTimeValue = moment(endTimeAttr); + initialCountdown = endTimeValue.diff(startValue, 'seconds'); + displayProgressBar = remainingTime * 100 / initialCountdown; + } + else { + displayProgressBar = remainingTime * 100 / coutdown; + } + + displayProgressBar = 100 - displayProgressBar; //To have 0 to 100 and not 100 to 0 + displayProgressBar = Math.round(displayProgressBar * 10) / 10; //learn more why : http://stackoverflow.com/questions/588004/is-floating-point-math-broken + + if(displayProgressBar > 100){ //security + displayProgressBar = 100; + } + + return displayProgressBar; + }; + + return new ProgressBarService(); +}); diff --git a/app/js/timer.js b/app/js/timer.js deleted file mode 100644 index 6c16eb9..0000000 --- a/app/js/timer.js +++ /dev/null @@ -1,147 +0,0 @@ -angular.module('timer', []) - .directive('timer', ['$compile', function ($compile) { - return { - restrict: 'E', - replace: false, - scope: { - interval: '=interval', - startTimeAttr: '=startTime', - endTimeAttr: '=endTime', - countdownattr: '=countdown', - autoStart: '&autoStart' - }, - controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) { - - //angular 1.2 doesn't support attributes ending in "-start", so we're - //supporting both "autostart" and "auto-start" as a solution for - //backward and forward compatibility. - $scope.autoStart = $attrs.autoStart || $attrs.autostart; - - if ($element.html().trim().length === 0) { - $element.append($compile('{{millis}}')($scope)); - } else { - $element.append($compile($element.contents())($scope)); - } - - $scope.startTime = null; - $scope.endTime = null; - $scope.timeoutId = null; - $scope.countdown = $scope.countdownattr && parseInt($scope.countdownattr, 10) >= 0 ? parseInt($scope.countdownattr, 10) : undefined; - $scope.isRunning = false; - - $scope.$on('timer-start', function () { - $scope.start(); - }); - - $scope.$on('timer-resume', function () { - $scope.resume(); - }); - - $scope.$on('timer-stop', function () { - $scope.stop(); - }); - - function resetTimeout() { - if ($scope.timeoutId) { - clearTimeout($scope.timeoutId); - } - } - - $scope.start = $element[0].start = function () { - $scope.startTime = $scope.startTimeAttr ? new Date($scope.startTimeAttr) : new Date(); - $scope.endTime = $scope.endTimeAttr ? new Date($scope.endTimeAttr) : null; - $scope.countdown = $scope.countdownattr && parseInt($scope.countdownattr, 10) > 0 ? parseInt($scope.countdownattr, 10) : undefined; - resetTimeout(); - tick(); - }; - - $scope.resume = $element[0].resume = function () { - resetTimeout(); - if ($scope.countdownattr) { - $scope.countdown += 1; - } - $scope.startTime = new Date() - ($scope.stoppedTime - $scope.startTime); - tick(); - }; - - $scope.stop = $scope.pause = $element[0].stop = $element[0].pause = function () { - $scope.stoppedTime = new Date(); - resetTimeout(); - $scope.$emit('timer-stopped', {millis: $scope.millis, seconds: $scope.seconds, minutes: $scope.minutes, hours: $scope.hours, days: $scope.days}); - $scope.timeoutId = null; - }; - - $element.bind('$destroy', function () { - resetTimeout(); - }); - - function calculateTimeUnits() { - $scope.seconds = Math.floor(($scope.millis / 1000) % 60); - $scope.minutes = Math.floor((($scope.millis / (60000)) % 60)); - $scope.hours = Math.floor((($scope.millis / (3600000)) % 24)); - $scope.days = Math.floor((($scope.millis / (3600000)) / 24)); - } - - //determine initial values of time units and add AddSeconds functionality - if ($scope.countdownattr) { - $scope.millis = $scope.countdownattr * 1000; - - $scope.addCDSeconds = $element[0].addCDSeconds = function( extraSeconds ){ - $scope.countdown += extraSeconds; - $scope.$digest(); - }; - $scope.$on('timer-add-time', function ( extraSeconds ) { - $scope.addSeconds( extraSeconds ); - }); - - - } else { - $scope.millis = 0; - } - calculateTimeUnits(); - - var tick = function () { - - $scope.millis = new Date() - $scope.startTime; - var adjustment = $scope.millis % 1000; - - if ($scope.endTimeAttr) { - $scope.millis = $scope.endTime - new Date(); - adjustment = $scope.interval - $scope.millis % 1000; - } - - - if ($scope.countdownattr) { - $scope.millis = $scope.countdown * 1000; - } - - if ($scope.millis < 0) { - $scope.stop(); - $scope.millis = 0; - calculateTimeUnits(); - return; - } - calculateTimeUnits(); - if ($scope.countdown > 0) { - $scope.countdown--; - } - else if ($scope.countdown <= 0) { - $scope.stop(); - return; - } - - //We are not using $timeout for a reason. Please read here - https://github.com/siddii/angular-timer/pull/5 - $scope.timeoutId = setTimeout(function () { - tick(); - $scope.$digest(); - }, $scope.interval - adjustment); - - $scope.$emit('timer-tick', {timeoutId: $scope.timeoutId, millis: $scope.millis}); - }; - - if ($scope.autoStart === undefined || $scope.autoStart === true) { - $scope.start(); - } - }] - }; - }]); diff --git a/bower.json b/bower.json index 3ca7200..829ba31 100644 --- a/bower.json +++ b/bower.json @@ -1,7 +1,7 @@ { "author": "Siddique Hameed", "name": "angular-timer", - "version": "1.0.10", + "version": "1.3.5", "homepage": "https://github.com/siddii/angular-timer", "description": "Angular-Timer : A simple AngularJS directive demonstrating re-usability & interoperability", "repository": { @@ -9,13 +9,21 @@ "url": "git://github.com/siddii/angular-timer.git" }, "dependencies": { - "angular": ">= 1.0.7" + "angular": ">= 1.0.7", + "moment": "~2.11.2", + "humanize-duration": "~3.10.0" }, "devDependencies": { - "bootstrap": "2.3.2", - "angular-scenario": ">= 1.0.7", - "angular-mocks": ">= 1.0.7" + "bootstrap": "2.3.2", + "angular-scenario": ">= 1.0.7", + "angular-mocks": ">= 1.0.7" }, "main": "./dist/angular-timer.js", - "ignore": ["./node_modules/", "./bower_components/"] + "ignore": [ + "node_modules", + "bower_components" + ], + "resolutions": { + "moment": "~2.9.0" + } } diff --git a/bower_components/angular-mocks/.bower.json b/bower_components/angular-mocks/.bower.json index 0cfe175..8dbeca1 100644 --- a/bower_components/angular-mocks/.bower.json +++ b/bower_components/angular-mocks/.bower.json @@ -1,16 +1,17 @@ { "name": "angular-mocks", - "version": "1.2.0-rc.2", + "version": "1.3.14", "main": "./angular-mocks.js", + "ignore": [], "dependencies": { - "angular": "1.2.0-rc.2" + "angular": "1.3.14" }, "homepage": "https://github.com/angular/bower-angular-mocks", - "_release": "1.2.0-rc.2", + "_release": "1.3.14", "_resolution": { "type": "version", - "tag": "v1.2.0-rc.2", - "commit": "9bdf39463a7e59c35f4f6163853c8da4fbf81ea3" + "tag": "v1.3.14", + "commit": "b33962810730adca9a0f7165ecd4835b6bf40abb" }, "_source": "git://github.com/angular/bower-angular-mocks.git", "_target": ">= 1.0.7", diff --git a/bower_components/angular-mocks/README.md b/bower_components/angular-mocks/README.md index 69bc520..440cce9 100644 --- a/bower_components/angular-mocks/README.md +++ b/bower_components/angular-mocks/README.md @@ -1,4 +1,63 @@ -bower-angular-mocks -=================== +# packaged angular-mocks -angular-mocks.js bower repo \ No newline at end of file +This repo is for distribution on `npm` and `bower`. The source for this module is in the +[main AngularJS repo](https://github.com/angular/angular.js/tree/master/src/ngMock). +Please file issues and pull requests against that repo. + +## Install + +You can install this package either with `npm` or with `bower`. + +### npm + +```shell +npm install angular-mocks +``` + +You can `require` ngMock modules: + +```js +var angular = require('angular'); +angular.module('myMod', [ + require('angular-animate'), + require('angular-mocks/ngMock') + require('angular-mocks/ngAnimateMock') +]); +``` + +### bower + +```shell +bower install angular-mocks +``` + +The mocks are then available at `bower_components/angular-mocks/angular-mocks.js`. + +## Documentation + +Documentation is available on the +[AngularJS docs site](https://docs.angularjs.org/guide/unit-testing). + +## License + +The MIT License + +Copyright (c) 2010-2015 Google, Inc. http://angularjs.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/bower_components/angular-mocks/angular-mocks.js b/bower_components/angular-mocks/angular-mocks.js old mode 100755 new mode 100644 index 993912e..b6bd983 --- a/bower_components/angular-mocks/angular-mocks.js +++ b/bower_components/angular-mocks/angular-mocks.js @@ -1,13 +1,14 @@ /** - * @license AngularJS v1.2.0-rc.2 - * (c) 2010-2012 Google, Inc. http://angularjs.org + * @license AngularJS v1.3.14 + * (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 @@ -52,9 +53,10 @@ angular.mock.$Browser = function() { self.onUrlChange = function(listener) { self.pollFns.push( function() { - if (self.$$lastUrl != self.$$url) { + if (self.$$lastUrl !== self.$$url || self.$$state !== self.$$lastState) { self.$$lastUrl = self.$$url; - listener(self.$$url); + self.$$lastState = self.$$state; + listener(self.$$url, self.$$state); } } ); @@ -62,6 +64,8 @@ angular.mock.$Browser = function() { return listener; }; + self.$$checkUrlChange = angular.noop; + self.cookieHash = {}; self.lastCookieHash = {}; self.deferredFns = []; @@ -70,11 +74,17 @@ angular.mock.$Browser = function() { self.defer = function(fn, delay) { delay = delay || 0; self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId}); - self.deferredFns.sort(function(a,b){ return a.time - b.time;}); + self.deferredFns.sort(function(a, b) { return a.time - b.time;}); return self.deferredNextId++; }; + /** + * @name $browser#defer.now + * + * @description + * Current milliseconds mock time. + */ self.defer.now = 0; @@ -95,8 +105,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. @@ -108,9 +117,9 @@ angular.mock.$Browser = function() { self.defer.now += delay; } else { if (self.deferredFns.length) { - self.defer.now = self.deferredFns[self.deferredFns.length-1].time; + 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'); } } @@ -119,30 +128,7 @@ angular.mock.$Browser = function() { } }; - /** - * @name ngMock.$browser#defer.flushNext - * @methodOf ngMock.$browser - * - * @description - * Flushes next pending request and compares it to the provided delay - * - * @param {number=} expectedDelay the delay value that will be asserted against the delay of the next timeout function - */ - self.defer.flushNext = function(expectedDelay) { - var tick = self.deferredFns.shift(); - expect(tick.time).toEqual(expectedDelay); - tick.fn(); - }; - - /** - * @name ngMock.$browser#defer.now - * @propertyOf ngMock.$browser - * - * @description - * Current milliseconds mock time. - */ - - self.$$baseHref = ''; + self.$$baseHref = '/'; self.baseHref = function() { return this.$$baseHref; }; @@ -150,14 +136,13 @@ angular.mock.$Browser = function() { angular.mock.$Browser.prototype = { /** - * @name ngMock.$browser#poll - * @methodOf ngMock.$browser + * @name $browser#poll * * @description * run all fns in pollFns */ poll: function poll() { - angular.forEach(this.pollFns, function(pollFn){ + angular.forEach(this.pollFns, function(pollFn) { pollFn(); }); }, @@ -167,18 +152,27 @@ angular.mock.$Browser.prototype = { return pollFn; }, - url: function(url, replace) { + url: function(url, replace, state) { + if (angular.isUndefined(state)) { + state = null; + } if (url) { this.$$url = url; + // Native pushState serializes & copies the object; simulate it. + this.$$state = angular.copy(state); return this; } return this.$$url; }, + state: function() { + return this.$$state; + }, + cookies: function(name, value) { if (name) { - if (value == undefined) { + if (angular.isUndefined(value)) { delete this.cookieHash[name]; } else { if (angular.isString(value) && //strings only @@ -202,25 +196,25 @@ 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 to the `$exceptionHandler`. */ /** - * @ngdoc object - * @name ngMock.$exceptionHandler + * @ngdoc service + * @name $exceptionHandler * * @description * Mock implementation of {@link ng.$exceptionHandler} that rethrows or logs errors passed - * into it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration + * to it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration * information. * * - *
+ * ```js
* describe('$exceptionHandlerProvider', function() {
*
* it('should capture log messages and exceptions', function() {
@@ -241,7 +235,7 @@ angular.mock.$Browser.prototype = {
* });
* });
* });
- *
+ * ```
*/
angular.mock.$ExceptionHandlerProvider = function() {
@@ -249,44 +243,42 @@ angular.mock.$ExceptionHandlerProvider = function() {
/**
* @ngdoc method
- * @name ngMock.$exceptionHandlerProvider#mode
- * @methodOf ngMock.$exceptionHandlerProvider
+ * @name $exceptionHandlerProvider#mode
*
* @description
* Sets the logging mode.
*
* @param {string} mode Mode of operation, defaults to `rethrow`.
*
- * - `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()}
+ * - `rethrow`: If any errors are passed to 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.
+ * For any implementations that expect exceptions to be thrown, the `rethrow` mode
+ * will also maintain a log of thrown errors.
*/
this.mode = function(mode) {
- switch(mode) {
- case 'rethrow':
- handler = function(e) {
- throw e;
- };
- break;
+
+ switch (mode) {
case 'log':
+ case 'rethrow':
var errors = [];
-
handler = function(e) {
if (arguments.length == 1) {
errors.push(e);
} else {
errors.push([].slice.call(arguments, 0));
}
+ if (mode === "rethrow") {
+ throw e;
+ }
};
-
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!");
}
};
@@ -300,7 +292,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
@@ -316,15 +308,15 @@ angular.mock.$LogProvider = function() {
}
this.debugEnabled = function(flag) {
- if (angular.isDefined(flag)) {
- debug = flag;
- return this;
- } else {
- return debug;
- }
+ if (angular.isDefined(flag)) {
+ debug = flag;
+ return this;
+ } else {
+ return debug;
+ }
};
- this.$get = function () {
+ this.$get = function() {
var $log = {
log: function() { $log.log.logs.push(concat([], arguments, 0)); },
warn: function() { $log.warn.logs.push(concat([], arguments, 0)); },
@@ -339,110 +331,105 @@ angular.mock.$LogProvider = function() {
/**
* @ngdoc method
- * @name ngMock.$log#reset
- * @methodOf ngMock.$log
+ * @name $log#reset
*
* @description
* Reset all of the logging arrays to empty.
*/
- $log.reset = 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}.
+ * Array of messages logged using {@link ng.$log#log `log()`}.
*
* @example
- *
+ * ```js
* $log.log('Some Log');
* var first = $log.log.logs.unshift();
- *
+ * ```
*/
$log.log.logs = [];
/**
* @ngdoc property
- * @name ngMock.$log#info.logs
- * @propertyOf ngMock.$log
+ * @name $log#info.logs
*
* @description
- * Array of messages logged using {@link ngMock.$log#info}.
+ * Array of messages logged using {@link ng.$log#info `info()`}.
*
* @example
- *
+ * ```js
* $log.info('Some Info');
* var first = $log.info.logs.unshift();
- *
+ * ```
*/
$log.info.logs = [];
/**
* @ngdoc property
- * @name ngMock.$log#warn.logs
- * @propertyOf ngMock.$log
+ * @name $log#warn.logs
*
* @description
- * Array of messages logged using {@link ngMock.$log#warn}.
+ * Array of messages logged using {@link ng.$log#warn `warn()`}.
*
* @example
- *
+ * ```js
* $log.warn('Some Warning');
* var first = $log.warn.logs.unshift();
- *
+ * ```
*/
$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}.
+ * Array of messages logged using {@link ng.$log#error `error()`}.
*
* @example
- *
- * $log.log('Some Error');
+ * ```js
+ * $log.error('Some Error');
* var first = $log.error.logs.unshift();
- *
+ * ```
*/
$log.error.logs = [];
/**
* @ngdoc property
- * @name ngMock.$log#debug.logs
- * @propertyOf ngMock.$log
+ * @name $log#debug.logs
*
* @description
- * Array of messages logged using {@link ngMock.$log#debug}.
+ * Array of messages logged using {@link ng.$log#debug `debug()`}.
*
* @example
- *
+ * ```js
* $log.debug('Some Error');
* var first = $log.debug.logs.unshift();
- *
+ * ```
*/
- $log.debug.logs = []
+ $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 all of the logging methods have no logged messages. If any messages are present,
+ * an exception is thrown.
*/
$log.assertEmpty = function() {
var errors = [];
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 || ''));
+ angular.forEach(log, function(logItem) {
+ 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'));
}
@@ -454,242 +441,393 @@ 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 = ['$browser', '$rootScope', '$q', '$$q',
+ function($browser, $rootScope, $q, $$q) {
+ var repeatFns = [],
+ nextRepeatId = 0,
+ now = 0;
+
+ var $interval = function(fn, delay, count, invokeApply) {
+ var iteration = 0,
+ skipApply = (angular.isDefined(invokeApply) && !invokeApply),
+ deferred = (skipApply ? $$q : $q).defer(),
+ promise = deferred.promise;
+
+ 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) {
+ $browser.defer.flush();
+ } else {
+ $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' - } - } - - //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"); - }; - }); - - return self; - }; + return self; +}; - //make "tzDateInstance instanceof Date" return true - angular.mock.TzDate.prototype = Date.prototype; -})(); +//make "tzDateInstance instanceof Date" return true +angular.mock.TzDate.prototype = Date.prototype; +/* jshint +W101 */ -angular.mock.animate = angular.module('mock.animate', ['ng']) +angular.mock.animate = angular.module('ngAnimateMock', ['ng']) .config(['$provide', function($provide) { - $provide.decorator('$animate', function($delegate) { + var reflowQueue = []; + $provide.value('$$animateReflow', function(fn) { + var index = reflowQueue.length; + reflowQueue.push(fn); + return function cancel() { + reflowQueue.splice(index, 1); + }; + }); + + $provide.decorator('$animate', ['$delegate', '$$asyncCallback', '$timeout', '$browser', + function($delegate, $$asyncCallback, $timeout, $browser) { var animate = { - queue : [], - enabled : $delegate.enabled, - flushNext : function(name) { - var tick = animate.queue.shift(); - expect(tick.method).toBe(name); - tick.fn(); - return tick; + queue: [], + cancel: $delegate.cancel, + enabled: $delegate.enabled, + triggerCallbackEvents: function() { + $$asyncCallback.flush(); + }, + triggerCallbackPromise: function() { + $timeout.flush(0); + }, + triggerCallbacks: function() { + this.triggerCallbackEvents(); + this.triggerCallbackPromise(); + }, + triggerReflow: function() { + angular.forEach(reflowQueue, function(fn) { + fn(); + }); + reflowQueue = []; } }; - forEach(['enter','leave','move','addClass','removeClass'], function(method) { + angular.forEach( + ['animate','enter','leave','move','addClass','removeClass','setClass'], function(method) { animate[method] = function() { - var params = arguments; animate.queue.push({ - method : method, - params : params, - element : angular.isElement(params[0]) && params[0], - parent : angular.isElement(params[1]) && params[1], - after : angular.isElement(params[2]) && params[2], - fn : function() { - $delegate[method].apply($delegate, params); - } + event: method, + element: arguments[0], + options: arguments[arguments.length - 1], + args: arguments }); + return $delegate[method].apply($delegate, arguments); }; }); return animate; - }); + }]); }]); @@ -701,9 +839,11 @@ angular.mock.animate = angular.module('mock.animate', ['ng']) * * *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 @@ -733,7 +873,8 @@ angular.mock.dump = function(object) { } else if (object instanceof Error) { out = object.stack || ('' + object.name + ': ' + object.message); } else { - // TODO(i): this prevents methods to be logged, we should have a better way to serialize objects + // TODO(i): this prevents methods being logged, + // we should have a better way to serialize objects out = angular.toJson(object, true); } } else { @@ -746,13 +887,13 @@ angular.mock.dump = function(object) { function serializeScope(scope, offset) { offset = offset || ' '; var log = [offset + 'Scope(' + scope.$id + '): {']; - for ( var key in scope ) { - if (scope.hasOwnProperty(key) && !key.match(/^(\$|this)/)) { + for (var key in scope) { + if (Object.prototype.hasOwnProperty.call(scope, key) && !key.match(/^(\$|this)/)) { log.push(' ' + key + ': ' + angular.toJson(scope[key])); } } var child = scope.$$childHead; - while(child) { + while (child) { log.push(serializeScope(child, offset + ' ')); child = child.$$nextSibling; } @@ -762,8 +903,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}. @@ -772,8 +913,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. @@ -784,7 +925,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: @@ -851,20 +992,24 @@ 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 in unit testing a controller. - * First we create the controller under test + * The following code shows how to setup and use the mock backend when unit testing a controller. + * First we create the controller under test: * -
+ ```js
+ // The module code
+ angular
+ .module('MyApp', [])
+ .controller('MyController', MyController);
+
// The controller code
function MyController($scope, $http) {
var authToken;
@@ -885,20 +1030,24 @@ angular.mock.dump = function(object) {
});
};
}
-
+ ```
*
- * Now we setup the mock backend and create the test specs.
+ * Now we setup the mock backend and create the test specs:
*
-
+ ```js
// testing controller
describe('MyController', function() {
- var $httpBackend, $rootScope, createController;
+ var $httpBackend, $rootScope, createController, authRequestHandler;
+
+ // Set up the module
+ beforeEach(module('MyApp'));
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'});
+ authRequestHandler = $httpBackend.when('GET', '/auth.py')
+ .respond({userId: 'userX'}, {'A-Token': 'xxx'});
// Get hold of a scope (i.e. the root scope)
$rootScope = $injector.get('$rootScope');
@@ -924,6 +1073,18 @@ angular.mock.dump = function(object) {
});
+ it('should fail authentication', function() {
+
+ // Notice how you can change the response even after it was set
+ authRequestHandler.respond(401, '');
+
+ $httpBackend.expectGET('/auth.py');
+ var controller = createController();
+ $httpBackend.flush();
+ expect($rootScope.status).toBe('Failed...');
+ });
+
+
it('should send msg to server', function() {
var controller = createController();
$httpBackend.flush();
@@ -955,10 +1116,10 @@ angular.mock.dump = function(object) {
$httpBackend.flush();
});
});
-
+ ```
*/
angular.mock.$HttpBackendProvider = function() {
- this.$get = ['$rootScope', createHttpBackendMock];
+ this.$get = ['$rootScope', '$timeout', createHttpBackendMock];
};
/**
@@ -975,19 +1136,20 @@ angular.mock.$HttpBackendProvider = function() {
* @param {Object=} $browser Auto-flushing enabled if specified
* @return {Object} Instance of $httpBackend mock
*/
-function createHttpBackendMock($rootScope, $delegate, $browser) {
+function createHttpBackendMock($rootScope, $timeout, $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]
- : [200, status, data];
+ ? [status, data, headers, statusText]
+ : [200, status, data, headers];
};
}
@@ -1004,14 +1166,17 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
}
function wrapResponse(wrapped) {
- if (!$browser && timeout && timeout.then) timeout.then(handleTimeout);
+ if (!$browser && timeout) {
+ timeout.then ? timeout.then(handleTimeout) : $timeout(handleTimeout, timeout);
+ }
return handleResponse;
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() {
@@ -1032,7 +1197,8 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
if (!expectation.matchHeaders(headers))
throw new Error('Expected ' + expectation + ' with different headers\n' +
- 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' + prettyPrint(headers));
+ 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' +
+ prettyPrint(headers));
expectations.shift();
@@ -1051,7 +1217,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
($browser ? $browser.defer : responsesPush)(wrapResponse(definition));
} else if (definition.passThrough) {
$delegate(method, url, data, callback, headers, timeout, withCredentials);
- } else throw Error('No response defined !');
+ } else throw new Error('No response defined !');
return;
}
}
@@ -1063,36 +1229,44 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
/**
* @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|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @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
- * request is handled.
+ * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change 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). The respond method returns the
+ * `requestHandler` object for possible overrides.
*/
$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.passThrough = undefined;
+ definition.response = createResponse(status, data, headers, statusText);
+ return chain;
}
};
if ($browser) {
chain.passThrough = function() {
+ definition.response = undefined;
definition.passThrough = true;
+ return chain;
};
}
@@ -1102,225 +1276,244 @@ 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()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
- * @returns {requestHandler} Returns an object with `respond` method that control how a matched
- * request is handled.
+ * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled.
*/
/**
* @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()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
- * @returns {requestHandler} Returns an object with `respond` method that control how a matched
- * request is handled.
+ * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled.
*/
/**
* @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()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
- * @returns {requestHandler} Returns an object with `respond` method that control how a matched
- * request is handled.
+ * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled.
*/
/**
* @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|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @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.
+ * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled.
*/
/**
* @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|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @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.
+ * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled.
*/
/**
* @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()`.
*
- * @param {string|RegExp} url HTTP url.
- * @returns {requestHandler} Returns an object with `respond` method that control how a matched
- * request is handled.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
+ * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled.
*/
createShortMethods('when');
/**
* @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|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @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.
+ * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change 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). The respond method returns the
+ * `requestHandler` object for possible overrides.
*/
$httpBackend.expect = function(method, url, data, headers) {
- var expectation = new MockHttpExpectation(method, url, data, headers);
+ var expectation = new MockHttpExpectation(method, url, data, headers),
+ chain = {
+ respond: function(status, data, headers, statusText) {
+ expectation.response = createResponse(status, data, headers, statusText);
+ return chain;
+ }
+ };
+
expectations.push(expectation);
- return {
- respond: function(status, data, headers) {
- expectation.response = createResponse(status, data, headers);
- }
- };
+ return chain;
};
/**
* @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()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {Object=} headers HTTP headers.
- * @returns {requestHandler} Returns an object with `respond` method that control how a matched
- * request is handled. See #expect for more info.
+ * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled. See #expect for more info.
*/
/**
* @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()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {Object=} headers HTTP headers.
- * @returns {requestHandler} Returns an object with `respond` method that control how a matched
- * request is handled.
+ * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled.
*/
/**
* @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()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {Object=} headers HTTP headers.
- * @returns {requestHandler} Returns an object with `respond` method that control how a matched
- * request is handled.
+ * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled.
*/
/**
* @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|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @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.
+ * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled.
*/
/**
* @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|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @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.
+ * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled.
*/
/**
* @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|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @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.
+ * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled.
*/
/**
* @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()`.
*
- * @param {string|RegExp} url HTTP url.
- * @returns {requestHandler} Returns an object with `respond` method that control how a matched
- * request is handled.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
+ * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled.
*/
createShortMethods('expect');
/**
* @ngdoc method
- * @name ngMock.$httpBackend#flush
- * @methodOf ngMock.$httpBackend
+ * @name $httpBackend#flush
* @description
* Flushes all pending requests using the trained responses.
*
@@ -1328,13 +1521,13 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
* all pending requests will be flushed. If there are no pending requests when the flush method
* is called an exception is thrown (as this typically a sign of programming error).
*/
- $httpBackend.flush = function(count) {
- $rootScope.$digest();
- if (!responses.length) throw Error('No pending request to flush !');
+ $httpBackend.flush = function(count, digest) {
+ if (digest !== false) $rootScope.$digest();
+ if (!responses.length) throw new Error('No pending request to flush !');
- if (angular.isDefined(count)) {
+ if (angular.isDefined(count) && count !== null) {
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 {
@@ -1342,14 +1535,13 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
responses.shift()();
}
}
- $httpBackend.verifyNoOutstandingExpectation();
+ $httpBackend.verifyNoOutstandingExpectation(digest);
};
/**
* @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.
@@ -1357,12 +1549,12 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
* Typically, you would call this method following each test case that asserts requests using an
* "afterEach" clause.
*
- *
+ * ```js
* afterEach($httpBackend.verifyNoOutstandingExpectation);
- *
+ * ```
*/
- $httpBackend.verifyNoOutstandingExpectation = function() {
- $rootScope.$digest();
+ $httpBackend.verifyNoOutstandingExpectation = function(digest) {
+ if (digest !== false) $rootScope.$digest();
if (expectations.length) {
throw new Error('Unsatisfied requests: ' + expectations.join(', '));
}
@@ -1371,29 +1563,27 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
/**
* @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
@@ -1408,16 +1598,16 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
function createShortMethods(prefix) {
- angular.forEach(['GET', 'DELETE', 'JSONP'], function(method) {
+ angular.forEach(['GET', 'DELETE', 'JSONP', 'HEAD'], 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);
+ };
});
}
}
@@ -1438,6 +1628,7 @@ function MockHttpExpectation(method, url, data, headers) {
this.matchUrl = function(u) {
if (!url) return true;
if (angular.isFunction(url.test)) return url.test(u);
+ if (angular.isFunction(url)) return url(u);
return url == u;
};
@@ -1451,7 +1642,9 @@ function MockHttpExpectation(method, url, data, headers) {
if (angular.isUndefined(data)) return true;
if (data && angular.isFunction(data.test)) return data.test(d);
if (data && angular.isFunction(data)) return data(d);
- if (data && !angular.isString(data)) return angular.toJson(data) == d;
+ if (data && !angular.isString(data)) {
+ return angular.equals(angular.fromJson(angular.toJson(data)), angular.fromJson(d));
+ }
return data == d;
};
@@ -1460,6 +1653,10 @@ function MockHttpExpectation(method, url, data, headers) {
};
}
+function createMockXhr() {
+ return new MockXhr();
+}
+
function MockXhr() {
// hack for testing $http, $httpBackend
@@ -1482,7 +1679,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;
@@ -1511,20 +1709,19 @@ 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) {
+angular.mock.$TimeoutDecorator = ['$delegate', '$browser', function($delegate, $browser) {
/**
* @ngdoc method
- * @name ngMock.$timeout#flush
- * @methodOf ngMock.$timeout
+ * @name $timeout#flush
* @description
*
* Flushes the queue of pending tasks.
@@ -1537,22 +1734,7 @@ angular.mock.$TimeoutDecorator = function($delegate, $browser) {
/**
* @ngdoc method
- * @name ngMock.$timeout#flushNext
- * @methodOf ngMock.$timeout
- * @description
- *
- * Flushes the next timeout in the queue and compares it to the provided delay
- *
- * @param {number=} expectedDelay the delay value that will be asserted against the delay of the next timeout function
- */
- $delegate.flushNext = function(expectedDelay) {
- $browser.defer.flushNext(expectedDelay);
- };
-
- /**
- * @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.
@@ -1574,7 +1756,49 @@ angular.mock.$TimeoutDecorator = function($delegate, $browser) {
}
return $delegate;
-};
+}];
+
+angular.mock.$RAFDecorator = ['$delegate', 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 < length; i++) {
+ queue[i]();
+ }
+
+ queue = [];
+ };
+
+ return rafFn;
+}];
+
+angular.mock.$AsyncCallbackDecorator = ['$delegate', function($delegate) {
+ var callbacks = [];
+ var addFn = function(fn) {
+ callbacks.push(fn);
+ };
+ addFn.flush = function() {
+ angular.forEach(callbacks, function(fn) {
+ fn();
+ });
+ callbacks = [];
+ };
+ return addFn;
+}];
/**
*
@@ -1582,43 +1806,58 @@ angular.mock.$TimeoutDecorator = function($delegate, $browser) {
angular.mock.$RootElementProvider = function() {
this.$get = function() {
return angular.element('');
- }
+ };
};
/**
- * @ngdoc overview
+ * @ngdoc module
* @name ngMock
+ * @packageName angular-mocks
* @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);
+ $provide.decorator('$rootScope', angular.mock.$RootScopeDecorator);
+}]);
/**
- * @ngdoc overview
+ * @ngdoc module
* @name ngMockE2E
+ * @module ngMockE2E
+ * @packageName angular-mocks
* @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}.
@@ -1638,13 +1877,13 @@ angular.module('ngMockE2E', ['ng']).config(function($provide) {
* use the `passThrough` request handler of `when` instead of `respond`.
*
* Additionally, we don't want to manually have to flush mocked out requests like we do during unit
- * testing. For this reason the e2e $httpBackend automatically flushes mocked out requests
+ * testing. For this reason the e2e $httpBackend flushes mocked out requests
* automatically, closely simulating the behavior of the XMLHttpRequest object.
*
* 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'}];
@@ -1654,163 +1893,279 @@ 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.
*
* @param {string} method HTTP method.
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {(string|RegExp)=} data HTTP request body.
* @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` and `passThrough` methods that
- * control how a matched request is handled.
+ * control how a matched request is handled. You can save this object for later use and invoke
+ * `respond` or `passThrough` again in order to change 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.)
+ * - Both methods return the `requestHandler` object for possible overrides.
*/
/**
* @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()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
- * control how a matched request is handled.
+ * control how a matched request is handled. You can save this object for later use and invoke
+ * `respond` or `passThrough` again in order to change how a matched request is handled.
*/
/**
* @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()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
- * control how a matched request is handled.
+ * control how a matched request is handled. You can save this object for later use and invoke
+ * `respond` or `passThrough` again in order to change how a matched request is handled.
*/
/**
* @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()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
- * control how a matched request is handled.
+ * control how a matched request is handled. You can save this object for later use and invoke
+ * `respond` or `passThrough` again in order to change how a matched request is handled.
*/
/**
* @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()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {(string|RegExp)=} data HTTP request body.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
- * control how a matched request is handled.
+ * control how a matched request is handled. You can save this object for later use and invoke
+ * `respond` or `passThrough` again in order to change how a matched request is handled.
*/
/**
* @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()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {(string|RegExp)=} data HTTP request body.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
- * control how a matched request is handled.
+ * control how a matched request is handled. You can save this object for later use and invoke
+ * `respond` or `passThrough` again in order to change how a matched request is handled.
*/
/**
* @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()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {(string|RegExp)=} data HTTP request body.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
- * control how a matched request is handled.
+ * control how a matched request is handled. You can save this object for later use and invoke
+ * `respond` or `passThrough` again in order to change how a matched request is handled.
*/
/**
* @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()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
- * control how a matched request is handled.
+ * control how a matched request is handled. You can save this object for later use and invoke
+ * `respond` or `passThrough` again in order to change how a matched request is handled.
*/
angular.mock.e2e = {};
-angular.mock.e2e.$httpBackendDecorator = ['$rootScope', '$delegate', '$browser', createHttpBackendMock];
+angular.mock.e2e.$httpBackendDecorator =
+ ['$rootScope', '$timeout', '$delegate', '$browser', createHttpBackendMock];
+
+
+/**
+ * @ngdoc type
+ * @name $rootScope.Scope
+ * @module ngMock
+ * @description
+ * {@link ng.$rootScope.Scope Scope} type decorated with helper methods useful for testing. These
+ * methods are automatically available on any {@link ng.$rootScope.Scope Scope} instance when
+ * `ngMock` module is loaded.
+ *
+ * In addition to all the regular `Scope` methods, the following helper methods are available:
+ */
+angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) {
+
+ var $rootScopePrototype = Object.getPrototypeOf($delegate);
+ $rootScopePrototype.$countChildScopes = countChildScopes;
+ $rootScopePrototype.$countWatchers = countWatchers;
+
+ return $delegate;
+
+ // ------------------------------------------------------------------------------------------ //
+
+ /**
+ * @ngdoc method
+ * @name $rootScope.Scope#$countChildScopes
+ * @module ngMock
+ * @description
+ * Counts all the direct and indirect child scopes of the current scope.
+ *
+ * The current scope is excluded from the count. The count includes all isolate child scopes.
+ *
+ * @returns {number} Total number of child scopes.
+ */
+ function countChildScopes() {
+ // jshint validthis: true
+ var count = 0; // exclude the current scope
+ var pendingChildHeads = [this.$$childHead];
+ var currentScope;
+
+ while (pendingChildHeads.length) {
+ currentScope = pendingChildHeads.shift();
+
+ while (currentScope) {
+ count += 1;
+ pendingChildHeads.push(currentScope.$$childHead);
+ currentScope = currentScope.$$nextSibling;
+ }
+ }
-angular.mock.clearDataCache = function() {
- var key,
- cache = angular.element.cache;
+ return count;
+ }
- for(key in cache) {
- if (cache.hasOwnProperty(key)) {
- var handle = cache[key].handle;
- handle && angular.element(handle.elem).off();
- delete cache[key];
+ /**
+ * @ngdoc method
+ * @name $rootScope.Scope#$countWatchers
+ * @module ngMock
+ * @description
+ * Counts all the watchers of direct and indirect child scopes of the current scope.
+ *
+ * The watchers of the current scope are included in the count and so are all the watchers of
+ * isolate child scopes.
+ *
+ * @returns {number} Total number of watchers.
+ */
+ function countWatchers() {
+ // jshint validthis: true
+ var count = this.$$watchers ? this.$$watchers.length : 0; // include the current scope
+ var pendingChildHeads = [this.$$childHead];
+ var currentScope;
+
+ while (pendingChildHeads.length) {
+ currentScope = pendingChildHeads.shift();
+
+ while (currentScope) {
+ count += currentScope.$$watchers ? currentScope.$$watchers.length : 0;
+ pendingChildHeads.push(currentScope.$$childHead);
+ currentScope = currentScope.$$nextSibling;
+ }
}
+
+ return count;
}
-};
+}];
+if (window.jasmine || window.mocha) {
-(window.jasmine || window.mocha) && (function(window) {
+ var currentSpec = null,
+ annotatedFunctions = [],
+ isSpecRunning = function() {
+ return !!currentSpec;
+ };
+
+ angular.mock.$$annotate = angular.injector.$$annotate;
+ angular.injector.$$annotate = function(fn) {
+ if (typeof fn === 'function' && !fn.$inject) {
+ annotatedFunctions.push(fn);
+ }
+ return angular.mock.$$annotate.apply(this, arguments);
+ };
- var currentSpec = null;
- beforeEach(function() {
+ (window.beforeEach || window.setup)(function() {
+ annotatedFunctions = [];
currentSpec = this;
});
- afterEach(function() {
+ (window.afterEach || window.teardown)(function() {
var injector = currentSpec.$injector;
+ annotatedFunctions.forEach(function(fn) {
+ delete fn.$inject;
+ });
+
+ angular.forEach(currentSpec.$modules, function(module) {
+ if (module && module.$$hashKey) {
+ module.$$hashKey = undefined;
+ }
+ });
+
currentSpec.$injector = null;
currentSpec.$modules = null;
currentSpec = null;
@@ -1820,8 +2175,6 @@ angular.mock.clearDataCache = function() {
injector.get('$browser').pollFns.length = 0;
}
- angular.mock.clearDataCache();
-
// clean up jquery's fragment cache
angular.forEach(angular.element.fragments, function(val, key) {
delete angular.element.fragments[key];
@@ -1835,16 +2188,13 @@ angular.mock.clearDataCache = function() {
angular.callbacks.counter = 0;
});
- function isSpecRunning() {
- return currentSpec && (window.mocha || currentSpec.queue.running);
- }
-
/**
* @ngdoc function
* @name angular.mock.module
* @description
*
* *NOTE*: This function is also published on window for easy access.
+ * ```js
*
* angular.module('myApplicationModule', [])
* .value('mode', 'app')
@@ -1926,32 +2309,64 @@ angular.mock.clearDataCache = function() {
* 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 || [];
-
+ var strictDi = !!currentSpec.$injectorStrict;
modules.unshift('ngMock');
modules.unshift('ng');
var injector = currentSpec.$injector;
if (!injector) {
- injector = currentSpec.$injector = angular.injector(modules);
+ if (strictDi) {
+ // If strictDi is enabled, annotate the providerInjector blocks
+ angular.forEach(modules, function(moduleFn) {
+ if (typeof moduleFn === "function") {
+ angular.injector.$$annotate(moduleFn);
+ }
+ });
+ }
+ injector = currentSpec.$injector = angular.injector(modules, strictDi);
+ currentSpec.$injectorStrict = strictDi;
}
- for(var i = 0, ii = blockFns.length; i < ii; i++) {
+ for (var i = 0, ii = blockFns.length; i < ii; i++) {
+ if (currentSpec.$injectorStrict) {
+ // If the injector is strict / strictDi, and the spec wants to inject using automatic
+ // annotation, then annotate the function here.
+ injector.annotate(blockFns[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;
@@ -1959,4 +2374,23 @@ angular.mock.clearDataCache = function() {
}
}
};
-})(window);
+
+
+ angular.mock.inject.strictDi = function(value) {
+ value = arguments.length ? !!value : true;
+ return isSpecRunning() ? workFn() : workFn;
+
+ function workFn() {
+ if (value !== currentSpec.$injectorStrict) {
+ if (currentSpec.$injector) {
+ throw new Error('Injector already created, can not modify strict annotations');
+ } else {
+ currentSpec.$injectorStrict = value;
+ }
+ }
+ }
+ };
+}
+
+
+})(window, window.angular);
diff --git a/bower_components/angular-mocks/bower.json b/bower_components/angular-mocks/bower.json
index 2f51b41..50ee07e 100644
--- a/bower_components/angular-mocks/bower.json
+++ b/bower_components/angular-mocks/bower.json
@@ -1,8 +1,9 @@
{
"name": "angular-mocks",
- "version": "1.2.0-rc.2",
+ "version": "1.3.14",
"main": "./angular-mocks.js",
+ "ignore": [],
"dependencies": {
- "angular": "1.2.0-rc.2"
+ "angular": "1.3.14"
}
}
diff --git a/bower_components/angular-mocks/ngAnimateMock.js b/bower_components/angular-mocks/ngAnimateMock.js
new file mode 100644
index 0000000..6f99e62
--- /dev/null
+++ b/bower_components/angular-mocks/ngAnimateMock.js
@@ -0,0 +1,2 @@
+require('./angular-mocks');
+module.exports = 'ngAnimateMock';
diff --git a/bower_components/angular-mocks/ngMock.js b/bower_components/angular-mocks/ngMock.js
new file mode 100644
index 0000000..7944de7
--- /dev/null
+++ b/bower_components/angular-mocks/ngMock.js
@@ -0,0 +1,2 @@
+require('./angular-mocks');
+module.exports = 'ngMock';
diff --git a/bower_components/angular-mocks/ngMockE2E.js b/bower_components/angular-mocks/ngMockE2E.js
new file mode 100644
index 0000000..fc2e539
--- /dev/null
+++ b/bower_components/angular-mocks/ngMockE2E.js
@@ -0,0 +1,2 @@
+require('./angular-mocks');
+module.exports = 'ngMockE2E';
diff --git a/bower_components/angular-mocks/package.json b/bower_components/angular-mocks/package.json
new file mode 100644
index 0000000..ea35c4a
--- /dev/null
+++ b/bower_components/angular-mocks/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "angular-mocks",
+ "version": "1.3.14",
+ "description": "AngularJS mocks for testing",
+ "main": "angular-mocks.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/angular/angular.js.git"
+ },
+ "keywords": [
+ "angular",
+ "framework",
+ "browser",
+ "mocks",
+ "testing",
+ "client-side"
+ ],
+ "author": "Angular Core Team | t |