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/dist/README.md b/CHANGELOG.md similarity index 65% rename from dist/README.md rename to CHANGELOG.md index 16cc225..4fbf9af 100644 --- a/dist/README.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ ##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 @@ -48,4 +72,4 @@ * 'auto-start' attribute name change to 'autostart' in support of Angular 1.2. See #14 ###Version 1.0.3 -* Successful Bower integration! +* 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 ced1501..0000000 --- a/app/js/timer.js +++ /dev/null @@ -1,252 +0,0 @@ -var timerModule = angular.module('timer', []) - .directive('timer', ['$compile', function ($compile) { - return { - restrict: 'EAC', - replace: false, - scope: { - interval: '=interval', - startTimeAttr: '=startTime', - endTimeAttr: '=endTime', - countdownattr: '=countdown', - autoStart: '&autoStart', - maxTimeUnit: '=' - }, - controller: ['$scope', '$element', '$attrs', '$timeout', function ($scope, $element, $attrs, $timeout) { - - // 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; - - 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(); - }); - - $scope.$on('timer-clear', function () { - $scope.clear(); - }); - - $scope.$on('timer-set-countdown', function (e, countdown) { - $scope.countdown = countdown; - }); - - 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; - if (!$scope.countdown) { - $scope.countdown = $scope.countdownattr && parseInt($scope.countdownattr, 10) > 0 ? parseInt($scope.countdownattr, 10) : undefined; - } - resetTimeout(); - tick(); - $scope.isRunning = true; - }; - - $scope.resume = $element[0].resume = function () { - resetTimeout(); - if ($scope.countdownattr) { - $scope.countdown += 1; - } - $scope.startTime = new Date() - ($scope.stoppedTime - $scope.startTime); - tick(); - $scope.isRunning = true; - }; - - $scope.stop = $scope.pause = $element[0].stop = $element[0].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 = $element[0].clear = function () { - // same as stop but without the event being triggered - $scope.stoppedTime = new Date(); - resetTimeout(); - $scope.timeoutId = null; - $scope.isRunning = false; - }; - - $element.bind('$destroy', function () { - resetTimeout(); - $scope.isRunning = false; - }); - - function calculateTimeUnits() { - - // 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 - $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'; - //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 = $element[0].addCDSeconds = function (extraSeconds) { - $scope.countdown += extraSeconds; - $scope.$digest(); - if (!$scope.isRunning) { - $scope.start(); - } - }; - - $scope.$on('timer-add-cd-seconds', function (e, extraSeconds) { - $timeout(function () { - $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 () { - - $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(); - - //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.countdown > 0) { - $scope.countdown--; - } - else if ($scope.countdown <= 0) { - $scope.stop(); - } - }; - - if ($scope.autoStart === undefined || $scope.autoStart === true) { - $scope.start(); - } - }] - }; - }]); - -/* commonjs package manager support (eg componentjs) */ -if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports){ - module.exports = timerModule; -} diff --git a/bower.json b/bower.json index e876804..829ba31 100644 --- a/bower.json +++ b/bower.json @@ -1,7 +1,7 @@ { "author": "Siddique Hameed", "name": "angular-timer", - "version": "1.1.5", + "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 http://github.com/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 |