From 58bd6faa47e831b47306ee91282dee640e2ac5ff Mon Sep 17 00:00:00 2001 From: Kevan Ahlquist Date: Sat, 3 Jan 2015 16:49:28 -0600 Subject: [PATCH 1/5] Added initial files for build and test automation. --- .dockerignore | 1 + .gitignore | 2 + Dockerfile | 3 +- Makefile | 1 + app/app.js | 4 +- gruntFile.js | 151 ++++++++++++++++++++++++++++++++++++++++++++++++++ index.html | 22 ++------ package.json | 35 ++++++++++++ 8 files changed, 198 insertions(+), 21 deletions(-) create mode 100644 .dockerignore create mode 100644 gruntFile.js create mode 100644 package.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..3c3629e64 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +node_modules diff --git a/.gitignore b/.gitignore index 4a54b4a3d..8eaa7270a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ logs/* !.gitkeep dockerui *.esproj/* +node_modules +dist diff --git a/Dockerfile b/Dockerfile index dcdfcf2c0..31b680be2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,7 @@ FROM crosbymichael/golang -ADD . /app/ +COPY dockerui.go /app/ +COPY dist/ /app/ WORKDIR /app/ RUN go build dockerui.go EXPOSE 9000 diff --git a/Makefile b/Makefile index 69b7d36f5..0676b2ae3 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ OPEN = $(shell which xdg-open || which open) PORT ?= 9000 build: + grunt build docker build --rm -t dockerui . run: diff --git a/app/app.js b/app/app.js index 48fcc4075..c9c22ffd7 100644 --- a/app/app.js +++ b/app/app.js @@ -1,6 +1,6 @@ 'use strict'; -angular.module('dockerui', ['ngRoute', 'dockerui.services', 'dockerui.filters', 'masthead', 'footer', 'dashboard', 'container', 'containers', 'images', 'image', 'startContainer', 'sidebar', 'settings', 'builder']) +angular.module('<%= pkg.name %>', ['<%= pkg.name %>.templates', 'ngRoute', '<%= pkg.name %>.services', '<%= pkg.name %>.filters', 'masthead', 'footer', 'dashboard', 'container', 'containers', 'images', 'image', 'startContainer', 'sidebar', 'settings', 'builder']) .config(['$routeProvider', function ($routeProvider) { $routeProvider.when('/', {templateUrl: 'app/components/dashboard/dashboard.html', controller: 'DashboardController'}); $routeProvider.when('/containers/', {templateUrl: 'app/components/containers/containers.html', controller: 'ContainersController'}); @@ -14,5 +14,5 @@ angular.module('dockerui', ['ngRoute', 'dockerui.services', 'dockerui.filters', // You need to set this to the api endpoint without the port i.e. http://192.168.1.9 .constant('DOCKER_ENDPOINT', '/dockerapi') .constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is requred. If you have a port, prefix it with a ':' i.e. :4243 - .constant('UI_VERSION', 'v0.5') + .constant('UI_VERSION', 'v<%= pkg.version %>') .constant('DOCKER_API_VERSION', 'v1.15'); diff --git a/gruntFile.js b/gruntFile.js new file mode 100644 index 000000000..398794ec8 --- /dev/null +++ b/gruntFile.js @@ -0,0 +1,151 @@ +module.exports = function (grunt) { + + grunt.loadNpmTasks('grunt-contrib-concat'); + grunt.loadNpmTasks('grunt-contrib-jshint'); + grunt.loadNpmTasks('grunt-contrib-uglify'); + grunt.loadNpmTasks('grunt-contrib-clean'); + grunt.loadNpmTasks('grunt-contrib-copy'); + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-recess'); + grunt.loadNpmTasks('grunt-karma'); + grunt.loadNpmTasks('grunt-html2js'); + + // Default task. + grunt.registerTask('default', ['jshint','build','karma:unit']); + grunt.registerTask('build', ['clean','html2js','concat','recess:build', 'copy']); + grunt.registerTask('release', ['clean','html2js','uglify','jshint','karma:unit','concat:index', 'recess:min', 'copy']); + grunt.registerTask('test-watch', ['karma:watch']); + + // Print a timestamp (useful for when watching) + grunt.registerTask('timestamp', function() { + grunt.log.subhead(Date()); + }); + + var karmaConfig = function(configFile, customOptions) { + var options = { configFile: configFile, keepalive: true }; + var travisOptions = process.env.TRAVIS && { browsers: ['Firefox'], reporters: 'dots' }; + return grunt.util._.extend(options, customOptions, travisOptions); + }; + + // Project configuration. + grunt.initConfig({ + distdir: 'dist', + pkg: grunt.file.readJSON('package.json'), + banner: + '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>\n' + + '<%= pkg.homepage ? " * " + pkg.homepage + "\\n" : "" %>' + + ' * Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author %>;\n' + + ' * Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %>\n */\n', + src: { + js: ['app/**/*.js'], + jsTpl: ['<%= distdir %>/templates/**/*.js'], + specs: ['test/**/*.spec.js'], + scenarios: ['test/**/*.scenario.js'], + html: ['index.html'], + tpl: { + app: ['app/components/**/*.html'] + }, + css: ['assets/css/app.css'] + }, + clean: ['<%= distdir %>/*'], + copy: { + assets: { + files: [{ dest: '<%= distdir %>/assets', src : '**', expand: true, cwd: 'assets/' }] + } + }, + karma: { + unit: { options: karmaConfig('test/config/unit.js') }, + watch: { options: karmaConfig('test/config/unit.js', { singleRun:false, autoWatch: true}) } + }, + html2js: { + app: { + options: { + base: '.' + }, + src: ['<%= src.tpl.app %>'], + dest: '<%= distdir %>/templates/app.js', + module: '<%= pkg.name %>.templates' + } + }, + concat:{ + dist:{ + options: { + banner: "<%= banner %>", + process: true + }, + src:['<%= src.js %>', '<%= src.jsTpl %>'], + dest:'<%= distdir %>/<%= pkg.name %>.js' + }, + index: { + src: ['index.html'], + dest: '<%= distdir %>/index.html', + options: { + process: true + } + }, + angular: { + src:['assets/js/angularjs/1.2.6/angular.min.js', + 'assets/js/angularjs/1.2.6/angular-route.min.js', + 'assets/js/angularjs/1.2.6/angular-resource.min.js'], + dest: '<%= distdir %>/angular.js' + } + }, + uglify: { + dist:{ + options: { + banner: "<%= banner %>" + }, + src:['<%= src.js %>' ,'<%= src.jsTpl %>'], + dest:'<%= distdir %>/<%= pkg.name %>.js' + }, + angular: { + src:['<%= concat.angular.src %>'], + dest: '<%= distdir %>/angular.js' + } + }, + recess: { + build: { + files: { + '<%= distdir %>/<%= pkg.name %>.css': + ['<%= src.css %>'] }, + options: { + compile: true, + noOverqualifying: false // TODO: Added because of .nav class, rename + } + }, + min: { + files: { + '<%= distdir %>/<%= pkg.name %>.css': ['<%= src.css %>'] + }, + options: { + compress: true + } + } + }, + watch:{ + all: { + files:['<%= src.js %>', '<%= src.specs %>', '<%= src.css %>', '<%= src.tpl.app %>', '<%= src.tpl.common %>', '<%= src.html %>'], + tasks:['default','timestamp'] + }, + build: { + files:['<%= src.js %>', '<%= src.specs %>', '<%= src.css %>', '<%= src.tpl.app %>', '<%= src.tpl.common %>', '<%= src.html %>'], + tasks:['build','timestamp'] + } + }, + jshint:{ + files:['gruntFile.js', '<%= src.js %>', '<%= src.jsTpl %>', '<%= src.specs %>', '<%= src.scenarios %>'], + options:{ + curly:true, + eqeqeq:true, + immed:true, + latedef:true, + newcap:true, + noarg:true, + sub:true, + boss:true, + eqnull:true, + globals:{} + } + } + }); +}; diff --git a/index.html b/index.html index 6307d2771..fb79d52d4 100644 --- a/index.html +++ b/index.html @@ -5,12 +5,13 @@ DockerUI - + - + + - - - - - - - - - - - + diff --git a/package.json b/package.json new file mode 100644 index 000000000..ce6e209d5 --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "author": "Michael Crosby", + "name": "dockerui", + "homepage": "https://github.com/crosbymichael/dockerui", + "version": "0.6.0-SNAPSHOT", + "repository": { + "type": "git", + "url": "git@github.com:crosbymichael/dockerui.git" + }, + "bugs": { + "url": "https://github.com/crosbymichael/dockerui/issues" + }, + "licenses": [ + { + "type": "MIT", + "url": "https://raw.githubusercontent.com/crosbymichael/dockerui/master/LICENSE" + } + ], + "engines": { + "node": ">= 0.8.4" + }, + "dependencies": {}, + "devDependencies": { + "grunt": "~0.4.0", + "grunt-recess": "~0.3", + "grunt-contrib-clean": "~0.4.0", + "grunt-contrib-copy": "~0.4.0", + "grunt-contrib-jshint": "~0.2.0", + "grunt-contrib-concat": "~0.1.3", + "grunt-contrib-uglify": "~0.1.1", + "grunt-karma": "~0.4.4", + "grunt-html2js": "~0.1.0", + "grunt-contrib-watch": "~0.3.1" + } +} From ab2819addd159b9ada39f812755876a939c1334a Mon Sep 17 00:00:00 2001 From: Kevan Ahlquist Date: Sat, 3 Jan 2015 18:39:40 -0600 Subject: [PATCH 2/5] Cleaned up linter errors. --- app/app.js | 3 +- app/components/container/container.html | 8 ++--- .../containerLogs/containerLogsController.js | 10 +++--- app/components/containers/containers.html | 2 +- .../dashboard/dashboardController.js | 2 +- app/components/image/imageController.js | 2 +- app/shared/filters.js | 33 ++++++++++++------- app/shared/services.js | 25 +++++++++----- assets/css/app.css | 30 ++++------------- gruntFile.js | 5 ++- partials/.gitkeep | 0 11 files changed, 62 insertions(+), 58 deletions(-) delete mode 100644 partials/.gitkeep diff --git a/app/app.js b/app/app.js index 7d7608859..6462a7584 100644 --- a/app/app.js +++ b/app/app.js @@ -1,7 +1,6 @@ -'use strict'; - angular.module('<%= pkg.name %>', ['<%= pkg.name %>.templates', 'ngRoute', '<%= pkg.name %>.services', '<%= pkg.name %>.filters', 'masthead', 'footer', 'dashboard', 'container', 'containers', 'images', 'image', 'startContainer', 'sidebar', 'settings', 'builder', 'containerLogs']) .config(['$routeProvider', function ($routeProvider) { + 'use strict'; $routeProvider.when('/', {templateUrl: 'app/components/dashboard/dashboard.html', controller: 'DashboardController'}); $routeProvider.when('/containers/', {templateUrl: 'app/components/containers/containers.html', controller: 'ContainersController'}); $routeProvider.when('/containers/:id/', {templateUrl: 'app/components/container/container.html', controller: 'ContainerController'}); diff --git a/app/components/container/container.html b/app/components/container/container.html index 82edb403a..6bacb011e 100644 --- a/app/components/container/container.html +++ b/app/components/container/container.html @@ -70,10 +70,10 @@ Hostname: {{ container.Config.Hostname }} - - IPAddress: - {{ container.NetworkSettings.IPAddress }} - + + IPAddress: + {{ container.NetworkSettings.IPAddress }} + Cmd: {{ container.Config.Cmd }} diff --git a/app/components/containerLogs/containerLogsController.js b/app/components/containerLogs/containerLogsController.js index 2b1bd3c87..ddd614d79 100644 --- a/app/components/containerLogs/containerLogsController.js +++ b/app/components/containerLogs/containerLogsController.js @@ -1,7 +1,7 @@ angular.module('containerLogs', []) .controller('ContainerLogsController', ['$scope', '$routeParams', '$location', '$anchorScroll', 'ContainerLogs', 'Container', 'ViewSpinner', function($scope, $routeParams, $location, $anchorScroll, ContainerLogs, Container, ViewSpinner) { - $scope.stdout = ''; + $scope.stdout = ''; $scope.stderr = ''; $scope.showTimestamps = false; @@ -20,7 +20,7 @@ function($scope, $routeParams, $location, $anchorScroll, ContainerLogs, Containe function getLogs() { ContainerLogs.get($routeParams.id, {stdout: 1, stderr: 0, timestamps: $scope.showTimestamps}, function(data, status, headers, config) { - // Replace carriage returns twith newlines to clean up output + // Replace carriage returns twith newlines to clean up output $scope.stdout = data.replace(/[\r]/g, '\n'); }); ContainerLogs.get($routeParams.id, {stdout: 0, stderr: 1}, function(data, status, headers, config) { @@ -40,9 +40,9 @@ function($scope, $routeParams, $location, $anchorScroll, ContainerLogs, Containe $scope.scrollTo = function(id) { $location.hash(id); $anchorScroll(); - } + }; $scope.toggleTimestamps = function() { - getLogs(); - } + getLogs(); + }; }]); diff --git a/app/components/containers/containers.html b/app/components/containers/containers.html index e5c36257c..a4f159f61 100644 --- a/app/components/containers/containers.html +++ b/app/components/containers/containers.html @@ -18,7 +18,7 @@
Display All + ng-change="toggleGetAll()"/> Display All
diff --git a/app/components/dashboard/dashboardController.js b/app/components/dashboard/dashboardController.js index 26b0ebeb2..2adad07cd 100644 --- a/app/components/dashboard/dashboardController.js +++ b/app/components/dashboard/dashboardController.js @@ -27,7 +27,7 @@ angular.module('dashboard', []) } Container.query({all: 1}, function(d) { - var running = 0 + var running = 0; var ghost = 0; var stopped = 0; diff --git a/app/components/image/imageController.js b/app/components/image/imageController.js index 6fc7a11b0..4e411dab9 100644 --- a/app/components/image/imageController.js +++ b/app/components/image/imageController.js @@ -36,7 +36,7 @@ function($scope, $q, $routeParams, $location, Image, Container, Messages, LineCh var containers = []; for (var i = 0; i < d.length; i++) { var c = d[i]; - if (c.Image == tag) { + if (c.Image === tag) { containers.push(new ContainerViewModel(c)); } } diff --git a/app/shared/filters.js b/app/shared/filters.js index 6c013a431..2efe3b784 100644 --- a/app/shared/filters.js +++ b/app/shared/filters.js @@ -1,13 +1,14 @@ -'use strict'; - angular.module('dockerui.filters', []) .filter('truncate', function() { + 'use strict'; return function(text, length, end) { - if (isNaN(length)) + if (isNaN(length)) { length = 10; + } - if (end === undefined) + if (end === undefined){ end = "..."; + } if (text.length <= length || text.length - end.length <= length) { return text; @@ -18,19 +19,22 @@ angular.module('dockerui.filters', []) }; }) .filter('statusbadge', function() { + 'use strict'; return function(text) { if (text === 'Ghost') { return 'important'; - } else if (text.indexOf('Exit') != -1 && text !== 'Exit 0') { + } else if (text.indexOf('Exit') !== -1 && text !== 'Exit 0') { return 'warning'; } return 'success'; }; }) .filter('getstatetext', function() { + 'use strict'; return function(state) { - if (state == undefined) return ''; - + if (state === undefined) { + return ''; + } if (state.Ghost && state.Running) { return 'Ghost'; } @@ -44,8 +48,11 @@ angular.module('dockerui.filters', []) }; }) .filter('getstatelabel', function() { + 'use strict'; return function(state) { - if (state == undefined) return ''; + if (state === undefined) { + return ''; + } if (state.Ghost && state.Running) { return 'label-important'; @@ -57,32 +64,36 @@ angular.module('dockerui.filters', []) }; }) .filter('humansize', function() { + 'use strict'; return function(bytes) { var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; - if (bytes == 0) { + if (bytes === 0) { return 'n/a'; } - var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); + var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)), 10); return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[[i]]; }; }) .filter('containername', function() { + 'use strict'; return function(container) { var name = container.Names[0]; return name.substring(1, name.length); }; }) .filter('repotag', function() { + 'use strict'; return function(image) { if (image.RepoTags && image.RepoTags.length > 0) { var tag = image.RepoTags[0]; - if (tag == ':') { tag = ''; } + if (tag === ':') { tag = ''; } return tag; } return ''; }; }) .filter('getdate', function() { + 'use strict'; return function(data) { //Multiply by 1000 for the unix format var date = new Date(data * 1000); diff --git a/app/shared/services.js b/app/shared/services.js index 57fd14e83..5a06bc5db 100644 --- a/app/shared/services.js +++ b/app/shared/services.js @@ -1,7 +1,6 @@ -'use strict'; - angular.module('dockerui.services', ['ngResource']) .factory('Container', function($resource, Settings) { + 'use strict'; // Resource for interacting with the docker containers // http://docs.docker.io/en/latest/api/docker_remote_api.html#containers return $resource(Settings.url + '/containers/:id/:action', { @@ -21,6 +20,7 @@ angular.module('dockerui.services', ['ngResource']) }); }) .factory('ContainerLogs', function($resource, $http, Settings) { + 'use strict'; return { get: function(id, params, callback) { $http({ @@ -31,9 +31,10 @@ angular.module('dockerui.services', ['ngResource']) console.log(error, data); }); } - } + }; }) .factory('Image', function($resource, Settings) { + 'use strict'; // Resource for docker images // http://docs.docker.io/en/latest/api/docker_remote_api.html#images return $resource(Settings.url + '/images/:id/:action', {}, { @@ -49,6 +50,7 @@ angular.module('dockerui.services', ['ngResource']) }); }) .factory('Docker', function($resource, Settings) { + 'use strict'; // Information for docker // http://docs.docker.io/en/latest/api/docker_remote_api.html#display-system-wide-information return $resource(Settings.url + '/version', {}, { @@ -56,6 +58,7 @@ angular.module('dockerui.services', ['ngResource']) }); }) .factory('Auth', function($resource, Settings) { + 'use strict'; // Auto Information for docker // http://docs.docker.io/en/latest/api/docker_remote_api.html#set-auth-configuration return $resource(Settings.url + '/auth', {}, { @@ -64,6 +67,7 @@ angular.module('dockerui.services', ['ngResource']) }); }) .factory('System', function($resource, Settings) { + 'use strict'; // System for docker // http://docs.docker.io/en/latest/api/docker_remote_api.html#display-system-wide-information return $resource(Settings.url + '/info', {}, { @@ -71,6 +75,7 @@ angular.module('dockerui.services', ['ngResource']) }); }) .factory('Settings', function(DOCKER_ENDPOINT, DOCKER_PORT, DOCKER_API_VERSION, UI_VERSION) { + 'use strict'; var url = DOCKER_ENDPOINT; if (DOCKER_PORT) { url = url + DOCKER_PORT + '\\' + DOCKER_PORT; @@ -82,10 +87,11 @@ angular.module('dockerui.services', ['ngResource']) rawUrl: DOCKER_ENDPOINT + DOCKER_PORT + '/' + DOCKER_API_VERSION, uiVersion: UI_VERSION, url: url, - firstLoad: true, + firstLoad: true }; }) .factory('ViewSpinner', function() { + 'use strict'; var spinner = new Spinner(); var target = document.getElementById('view'); @@ -95,6 +101,7 @@ angular.module('dockerui.services', ['ngResource']) }; }) .factory('Messages', function($rootScope) { + 'use strict'; return { send: function(title, text) { $.gritter.add({ @@ -102,7 +109,7 @@ angular.module('dockerui.services', ['ngResource']) text: text, time: 2000, before_open: function() { - if($('.gritter-item-wrapper').length == 3) { + if($('.gritter-item-wrapper').length === 3) { return false; } } @@ -114,7 +121,7 @@ angular.module('dockerui.services', ['ngResource']) text: text, time: 6000, before_open: function() { - if($('.gritter-item-wrapper').length == 4) { + if($('.gritter-item-wrapper').length === 4) { return false; } } @@ -123,6 +130,7 @@ angular.module('dockerui.services', ['ngResource']) }; }) .factory('Dockerfile', function(Settings) { + 'use strict'; var url = Settings.rawUrl + '/build'; return { build: function(file, callback) { @@ -138,6 +146,7 @@ angular.module('dockerui.services', ['ngResource']) }; }) .factory('LineChart', function(Settings) { + 'use strict'; var url = Settings.rawUrl + '/build'; return { build: function(id, data, getkey){ @@ -157,10 +166,10 @@ angular.module('dockerui.services', ['ngResource']) } var labels = []; - var data = []; + data = []; var keys = Object.keys(map); - for (var i = keys.length - 1; i > -1; i--) { + for (i = keys.length - 1; i > -1; i--) { var k = keys[i]; labels.push(k); data.push(map[k]); diff --git a/assets/css/app.css b/assets/css/app.css index 5fe8e24a8..6087c9c28 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -22,8 +22,8 @@ body { } .jumbotron .btn { - font-size: 21px; padding: 14px 24px; + font-size: 21px; } .marketing { @@ -46,15 +46,15 @@ body { .masthead .nav li { display: table-cell; - width: 1%; float: none; + width: 1%; } .masthead .nav li a { font-weight: bold; text-align: center; - border-left: 1px solid rgba(255,255,255,.75); border-right: 1px solid rgba(0,0,0,.1); + border-left: 1px solid rgba(255,255,255,.75); } .masthead .nav li:first-child a { @@ -81,8 +81,8 @@ body { } .btn-remove { - margin: 0 auto; max-width: 70%; + margin: 0 auto; } .actions { @@ -97,33 +97,15 @@ body { padding: 10px 15px 0 15px; } -#response { - width: 80%; - margin: 0 auto; -} - -#editor { - height: 300px; - width: 100%; - border: 1px solid #DDD; - margin-top: 5px; -} - .messages { max-height: 50px; - overflow-y: scroll; overflow-x: hidden; + overflow-y: scroll; } .legend .title { + padding: 0 0.3em; margin: 0.5em; border-style: solid; border-width: 0 0 0 1em; - padding: 0 0.3em; -} - -pre.pre-x-scrollable { - max-height: 700px; - word-wrap: normal; - white-space: pre; } diff --git a/gruntFile.js b/gruntFile.js index 398794ec8..2f20ccb5a 100644 --- a/gruntFile.js +++ b/gruntFile.js @@ -144,7 +144,10 @@ module.exports = function (grunt) { sub:true, boss:true, eqnull:true, - globals:{} + globals:{ + angular: false, + '$': false + } } } }); diff --git a/partials/.gitkeep b/partials/.gitkeep deleted file mode 100644 index e69de29bb..000000000 From 04418e2e686537362cfc000523dfc6fe3daaaafe Mon Sep 17 00:00:00 2001 From: Kevan Ahlquist Date: Sat, 3 Jan 2015 18:59:22 -0600 Subject: [PATCH 3/5] Added Karma config for unit tests. --- gruntFile.js | 4 +- test/assets/angular/angular-mocks.js | 2116 ++++++++++++++++++++++++++ test/unit/karma.conf.js | 56 + 3 files changed, 2174 insertions(+), 2 deletions(-) create mode 100644 test/assets/angular/angular-mocks.js create mode 100644 test/unit/karma.conf.js diff --git a/gruntFile.js b/gruntFile.js index 2f20ccb5a..7a96965de 100644 --- a/gruntFile.js +++ b/gruntFile.js @@ -54,8 +54,8 @@ module.exports = function (grunt) { } }, karma: { - unit: { options: karmaConfig('test/config/unit.js') }, - watch: { options: karmaConfig('test/config/unit.js', { singleRun:false, autoWatch: true}) } + unit: { options: karmaConfig('test/unit/karma.conf.js') }, + watch: { options: karmaConfig('test/unit/karma.conf.js', { singleRun:false, autoWatch: true}) } }, html2js: { app: { diff --git a/test/assets/angular/angular-mocks.js b/test/assets/angular/angular-mocks.js new file mode 100644 index 000000000..8a7d75088 --- /dev/null +++ b/test/assets/angular/angular-mocks.js @@ -0,0 +1,2116 @@ +/** + * @license AngularJS v1.2.6 + * (c) 2010-2014 Google, Inc. http://angularjs.org + * License: MIT + */ +(function(window, angular, undefined) { + +'use strict'; + +/** + * @ngdoc overview + * @name angular.mock + * @description + * + * Namespace from 'angular-mocks.js' which contains testing related code. + */ +angular.mock = {}; + +/** + * ! This is a private undocumented service ! + * + * @name ngMock.$browser + * + * @description + * This service is a mock implementation of {@link ng.$browser}. It provides fake + * implementation for commonly used browser apis that are hard to test, e.g. setTimeout, xhr, + * cookies, etc... + * + * The api of this service is the same as that of the real {@link ng.$browser $browser}, except + * that there are several helper methods available which can be used in tests. + */ +angular.mock.$BrowserProvider = function() { + this.$get = function() { + return new angular.mock.$Browser(); + }; +}; + +angular.mock.$Browser = function() { + var self = this; + + this.isMock = true; + self.$$url = "http://server/"; + self.$$lastUrl = self.$$url; // used by url polling fn + self.pollFns = []; + + // TODO(vojta): remove this temporary api + self.$$completeOutstandingRequest = angular.noop; + self.$$incOutstandingRequestCount = angular.noop; + + + // register url polling fn + + self.onUrlChange = function(listener) { + self.pollFns.push( + function() { + if (self.$$lastUrl != self.$$url) { + self.$$lastUrl = self.$$url; + listener(self.$$url); + } + } + ); + + return listener; + }; + + self.cookieHash = {}; + self.lastCookieHash = {}; + self.deferredFns = []; + self.deferredNextId = 0; + + 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;}); + return self.deferredNextId++; + }; + + + /** + * @name ngMock.$browser#defer.now + * @propertyOf ngMock.$browser + * + * @description + * Current milliseconds mock time. + */ + self.defer.now = 0; + + + self.defer.cancel = function(deferId) { + var fnIndex; + + angular.forEach(self.deferredFns, function(fn, index) { + if (fn.id === deferId) fnIndex = index; + }); + + if (fnIndex !== undefined) { + self.deferredFns.splice(fnIndex, 1); + return true; + } + + return false; + }; + + + /** + * @name ngMock.$browser#defer.flush + * @methodOf ngMock.$browser + * + * @description + * Flushes all pending requests and executes the defer callbacks. + * + * @param {number=} number of milliseconds to flush. See {@link #defer.now} + */ + self.defer.flush = function(delay) { + if (angular.isDefined(delay)) { + self.defer.now += delay; + } else { + if (self.deferredFns.length) { + self.defer.now = self.deferredFns[self.deferredFns.length-1].time; + } else { + throw new Error('No deferred tasks to be flushed'); + } + } + + while (self.deferredFns.length && self.deferredFns[0].time <= self.defer.now) { + self.deferredFns.shift().fn(); + } + }; + + self.$$baseHref = ''; + self.baseHref = function() { + return this.$$baseHref; + }; +}; +angular.mock.$Browser.prototype = { + +/** + * @name ngMock.$browser#poll + * @methodOf ngMock.$browser + * + * @description + * run all fns in pollFns + */ + poll: function poll() { + angular.forEach(this.pollFns, function(pollFn){ + pollFn(); + }); + }, + + addPollFn: function(pollFn) { + this.pollFns.push(pollFn); + return pollFn; + }, + + url: function(url, replace) { + if (url) { + this.$$url = url; + return this; + } + + return this.$$url; + }, + + cookies: function(name, value) { + if (name) { + if (angular.isUndefined(value)) { + delete this.cookieHash[name]; + } else { + if (angular.isString(value) && //strings only + value.length <= 4096) { //strict cookie storage limits + this.cookieHash[name] = value; + } + } + } else { + if (!angular.equals(this.cookieHash, this.lastCookieHash)) { + this.lastCookieHash = angular.copy(this.cookieHash); + this.cookieHash = angular.copy(this.cookieHash); + } + return this.cookieHash; + } + }, + + notifyWhenNoOutstandingRequests: function(fn) { + fn(); + } +}; + + +/** + * @ngdoc object + * @name ngMock.$exceptionHandlerProvider + * + * @description + * Configures the mock implementation of {@link ng.$exceptionHandler} to rethrow or to log errors + * passed into the `$exceptionHandler`. + */ + +/** + * @ngdoc object + * @name ngMock.$exceptionHandler + * + * @description + * Mock implementation of {@link ng.$exceptionHandler} that rethrows or logs errors passed + * into it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration + * information. + * + * + *
+ *   describe('$exceptionHandlerProvider', function() {
+ *
+ *     it('should capture log messages and exceptions', function() {
+ *
+ *       module(function($exceptionHandlerProvider) {
+ *         $exceptionHandlerProvider.mode('log');
+ *       });
+ *
+ *       inject(function($log, $exceptionHandler, $timeout) {
+ *         $timeout(function() { $log.log(1); });
+ *         $timeout(function() { $log.log(2); throw 'banana peel'; });
+ *         $timeout(function() { $log.log(3); });
+ *         expect($exceptionHandler.errors).toEqual([]);
+ *         expect($log.assertEmpty());
+ *         $timeout.flush();
+ *         expect($exceptionHandler.errors).toEqual(['banana peel']);
+ *         expect($log.log.logs).toEqual([[1], [2], [3]]);
+ *       });
+ *     });
+ *   });
+ * 
+ */ + +angular.mock.$ExceptionHandlerProvider = function() { + var handler; + + /** + * @ngdoc method + * @name ngMock.$exceptionHandlerProvider#mode + * @methodOf ngMock.$exceptionHandlerProvider + * + * @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()} + */ + this.mode = function(mode) { + switch(mode) { + case 'rethrow': + handler = function(e) { + throw e; + }; + break; + case 'log': + var errors = []; + + handler = function(e) { + if (arguments.length == 1) { + errors.push(e); + } else { + errors.push([].slice.call(arguments, 0)); + } + }; + + handler.errors = errors; + break; + default: + throw new Error("Unknown mode '" + mode + "', only 'log'/'rethrow' modes are allowed!"); + } + }; + + this.$get = function() { + return handler; + }; + + this.mode('rethrow'); +}; + + +/** + * @ngdoc service + * @name ngMock.$log + * + * @description + * Mock implementation of {@link ng.$log} that gathers all logged messages in arrays + * (one array per logging level). These arrays are exposed as `logs` property of each of the + * level-specific log function, e.g. for level `error` the array is exposed as `$log.error.logs`. + * + */ +angular.mock.$LogProvider = function() { + var debug = true; + + function concat(array1, array2, index) { + return array1.concat(Array.prototype.slice.call(array2, index)); + } + + this.debugEnabled = function(flag) { + if (angular.isDefined(flag)) { + debug = flag; + return this; + } else { + return debug; + } + }; + + this.$get = function () { + var $log = { + log: function() { $log.log.logs.push(concat([], arguments, 0)); }, + warn: function() { $log.warn.logs.push(concat([], arguments, 0)); }, + info: function() { $log.info.logs.push(concat([], arguments, 0)); }, + error: function() { $log.error.logs.push(concat([], arguments, 0)); }, + debug: function() { + if (debug) { + $log.debug.logs.push(concat([], arguments, 0)); + } + } + }; + + /** + * @ngdoc method + * @name ngMock.$log#reset + * @methodOf ngMock.$log + * + * @description + * Reset all of the logging arrays to empty. + */ + $log.reset = function () { + /** + * @ngdoc property + * @name ngMock.$log#log.logs + * @propertyOf ngMock.$log + * + * @description + * Array of messages logged using {@link ngMock.$log#log}. + * + * @example + *
+       * $log.log('Some Log');
+       * var first = $log.log.logs.unshift();
+       * 
+ */ + $log.log.logs = []; + /** + * @ngdoc property + * @name ngMock.$log#info.logs + * @propertyOf ngMock.$log + * + * @description + * Array of messages logged using {@link ngMock.$log#info}. + * + * @example + *
+       * $log.info('Some Info');
+       * var first = $log.info.logs.unshift();
+       * 
+ */ + $log.info.logs = []; + /** + * @ngdoc property + * @name ngMock.$log#warn.logs + * @propertyOf ngMock.$log + * + * @description + * Array of messages logged using {@link ngMock.$log#warn}. + * + * @example + *
+       * $log.warn('Some Warning');
+       * var first = $log.warn.logs.unshift();
+       * 
+ */ + $log.warn.logs = []; + /** + * @ngdoc property + * @name ngMock.$log#error.logs + * @propertyOf ngMock.$log + * + * @description + * Array of messages logged using {@link ngMock.$log#error}. + * + * @example + *
+       * $log.log('Some Error');
+       * var first = $log.error.logs.unshift();
+       * 
+ */ + $log.error.logs = []; + /** + * @ngdoc property + * @name ngMock.$log#debug.logs + * @propertyOf ngMock.$log + * + * @description + * Array of messages logged using {@link ngMock.$log#debug}. + * + * @example + *
+       * $log.debug('Some Error');
+       * var first = $log.debug.logs.unshift();
+       * 
+ */ + $log.debug.logs = []; + }; + + /** + * @ngdoc method + * @name ngMock.$log#assertEmpty + * @methodOf ngMock.$log + * + * @description + * Assert that the all of the logging methods have no logged messages. If messages present, an + * exception is thrown. + */ + $log.assertEmpty = function() { + var errors = []; + angular.forEach(['error', 'warn', 'info', 'log', '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 || '')); + }); + }); + }); + 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.push(''); + throw new Error(errors.join('\n---------\n')); + } + }; + + $log.reset(); + return $log; + }; +}; + + +/** + * @ngdoc service + * @name ngMock.$interval + * + * @description + * Mock implementation of the $interval service. + * + * Use {@link ngMock.$interval#methods_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#methods_$apply $apply} block. + * @returns {promise} A promise which will be notified on each iteration. + */ +angular.mock.$IntervalProvider = function() { + this.$get = ['$rootScope', '$q', + function($rootScope, $q) { + var repeatFns = [], + nextRepeatId = 0, + now = 0; + + var $interval = function(fn, delay, count, invokeApply) { + var deferred = $q.defer(), + promise = deferred.promise, + iteration = 0, + skipApply = (angular.isDefined(invokeApply) && !invokeApply); + + count = (angular.isDefined(count)) ? count : 0, + promise.then(null, null, fn); + + promise.$$intervalId = nextRepeatId; + + function tick() { + deferred.notify(iteration++); + + if (count > 0 && iteration >= count) { + var fnIndex; + deferred.resolve(iteration); + + angular.forEach(repeatFns, function(fn, index) { + if (fn.id === promise.$$intervalId) fnIndex = index; + }); + + if (fnIndex !== undefined) { + repeatFns.splice(fnIndex, 1); + } + } + + if (!skipApply) $rootScope.$apply(); + } + + repeatFns.push({ + nextTime:(now + delay), + delay: delay, + fn: tick, + id: nextRepeatId, + deferred: deferred + }); + repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;}); + + nextRepeatId++; + return promise; + }; + + $interval.cancel = function(promise) { + 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; + }; + + /** + * @ngdoc method + * @name ngMock.$interval#flush + * @methodOf ngMock.$interval + * @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;}); + } + 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]); + } + 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 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 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); + } + + var localOffset = new Date(timestamp).getTimezoneOffset(); + self.offsetDiff = localOffset*60*1000 - offset*1000*60*60; + self.date = new Date(timestamp + self.offsetDiff); + + self.getTime = function() { + return self.date.getTime() - self.offsetDiff; + }; + + self.toLocaleDateString = function() { + return self.date.toLocaleDateString(); + }; + + self.getFullYear = function() { + return self.date.getFullYear(); + }; + + self.getMonth = function() { + return self.date.getMonth(); + }; + + self.getDate = function() { + return self.date.getDate(); + }; + + self.getHours = function() { + return self.date.getHours(); + }; + + self.getMinutes = function() { + return self.date.getMinutes(); + }; + + self.getSeconds = function() { + return self.date.getSeconds(); + }; + + self.getMilliseconds = function() { + return self.date.getMilliseconds(); + }; + + self.getTimezoneOffset = function() { + return offset * 60; + }; + + self.getUTCFullYear = function() { + return self.origDate.getUTCFullYear(); + }; + + self.getUTCMonth = function() { + return self.origDate.getUTCMonth(); + }; + + self.getUTCDate = function() { + return self.origDate.getUTCDate(); + }; + + self.getUTCHours = function() { + return self.origDate.getUTCHours(); + }; + + self.getUTCMinutes = function() { + return self.origDate.getUTCMinutes(); + }; + + self.getUTCSeconds = function() { + return self.origDate.getUTCSeconds(); + }; + + self.getUTCMilliseconds = function() { + return self.origDate.getUTCMilliseconds(); + }; + + self.getDay = function() { + return self.date.getDay(); + }; + + // 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 new Error("Method '" + methodName + "' is not implemented in the TzDate mock"); + }; + }); + + return self; +}; + +//make "tzDateInstance instanceof Date" return true +angular.mock.TzDate.prototype = Date.prototype; +/* jshint +W101 */ + +angular.mock.animate = angular.module('mock.animate', ['ng']) + + .config(['$provide', function($provide) { + + $provide.decorator('$animate', function($delegate) { + var animate = { + queue : [], + enabled : $delegate.enabled, + flushNext : function(name) { + var tick = animate.queue.shift(); + + if (!tick) throw new Error('No animation to be flushed'); + if(tick.method !== name) { + throw new Error('The next animation is not "' + name + + '", but is "' + tick.method + '"'); + } + tick.fn(); + return tick; + } + }; + + angular.forEach(['enter','leave','move','addClass','removeClass'], 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); + } + }); + }; + }); + + return animate; + }); + + }]); + + +/** + * @ngdoc function + * @name angular.mock.dump + * @description + * + * *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. + * + * 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 + */ +angular.mock.dump = function(object) { + return serialize(object); + + function serialize(object) { + var out; + + if (angular.isElement(object)) { + object = angular.element(object); + out = angular.element('
'); + angular.forEach(object, function(element) { + out.append(angular.element(element).clone()); + }); + out = out.html(); + } else if (angular.isArray(object)) { + out = []; + angular.forEach(object, function(o) { + out.push(serialize(o)); + }); + out = '[ ' + out.join(', ') + ' ]'; + } else if (angular.isObject(object)) { + if (angular.isFunction(object.$eval) && angular.isFunction(object.$apply)) { + out = serializeScope(object); + } else if (object instanceof Error) { + out = object.stack || ('' + object.name + ': ' + object.message); + } else { + // TODO(i): this prevents methods being logged, + // we should have a better way to serialize objects + out = angular.toJson(object, true); + } + } else { + out = String(object); + } + + return out; + } + + function serializeScope(scope, offset) { + offset = offset || ' '; + var log = [offset + 'Scope(' + scope.$id + '): {']; + 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) { + log.push(serializeScope(child, offset + ' ')); + child = child.$$nextSibling; + } + log.push('}'); + return log.join('\n' + offset); + } +}; + +/** + * @ngdoc object + * @name ngMock.$httpBackend + * @description + * Fake HTTP backend implementation suitable for unit testing applications that use the + * {@link ng.$http $http service}. + * + * *Note*: For fake HTTP backend implementation suitable for end-to-end testing or backend-less + * 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 + * 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. + * + * This mock implementation can be used to respond with static or dynamic responses via the + * `expect` and `when` apis and their shortcuts (`expectGET`, `whenPOST`, etc). + * + * 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. + * + * 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: + * + * - `$httpBackend.expect` - specifies a request expectation + * - `$httpBackend.when` - specifies a backend definition + * + * + * # Request Expectations vs Backend Definitions + * + * Request expectations provide a way to make assertions about requests made by the application and + * to define responses for those requests. The test will fail if the expected requests are not made + * or they are made in the wrong order. + * + * Backend definitions allow you to define a fake backend for your application which doesn't assert + * if a particular request was made or not, it just returns a trained response if a request is made. + * The test will pass whether or not the request gets made during testing. + * + * + *
+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Request expectationsBackend definitions
Syntax.expect(...).respond(...).when(...).respond(...)
Typical usagestrict unit testsloose (black-box) unit testing
Fulfills multiple requestsNOYES
Order of requests mattersYESNO
Request requiredYESNO
Response requiredoptional (see below)YES
+ * + * In cases where both backend definitions and request expectations are specified during unit + * testing, the request expectations are evaluated first. + * + * If a request expectation has no response specified, the algorithm will search your backend + * definitions for an appropriate response. + * + * If a request didn't match any expectation or if the expectation doesn't have the response + * defined, the backend definitions are evaluated in sequential order to see if any of them match + * the request. The response from the first matched definition is returned. + * + * + * # 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. + * + * + * # 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 controller code
+  function MyController($scope, $http) {
+    var authToken;
+
+    $http.get('/auth.py').success(function(data, status, headers) {
+      authToken = headers('A-Token');
+      $scope.user = data;
+    });
+
+    $scope.saveMessage = function(message) {
+      var headers = { 'Authorization': authToken };
+      $scope.status = 'Saving...';
+
+      $http.post('/add-msg.py', message, { headers: headers } ).success(function(response) {
+        $scope.status = '';
+      }).error(function() {
+        $scope.status = 'ERROR!';
+      });
+    };
+  }
+  
+ * + * Now we setup the mock backend and create the test specs. + * +
+    // testing controller
+    describe('MyController', function() {
+       var $httpBackend, $rootScope, createController;
+
+       beforeEach(inject(function($injector) {
+         // Set up the mock http service responses
+         $httpBackend = $injector.get('$httpBackend');
+         // backend definition common for all tests
+         $httpBackend.when('GET', '/auth.py').respond({userId: 'userX'}, {'A-Token': 'xxx'});
+
+         // Get hold of a scope (i.e. the root scope)
+         $rootScope = $injector.get('$rootScope');
+         // The $controller service is used to create instances of controllers
+         var $controller = $injector.get('$controller');
+
+         createController = function() {
+           return $controller('MyController', {'$scope' : $rootScope });
+         };
+       }));
+
+
+       afterEach(function() {
+         $httpBackend.verifyNoOutstandingExpectation();
+         $httpBackend.verifyNoOutstandingRequest();
+       });
+
+
+       it('should fetch authentication token', function() {
+         $httpBackend.expectGET('/auth.py');
+         var controller = createController();
+         $httpBackend.flush();
+       });
+
+
+       it('should send msg to server', function() {
+         var controller = createController();
+         $httpBackend.flush();
+
+         // now you don’t care about the authentication, but
+         // the controller will still send the request and
+         // $httpBackend will respond without you having to
+         // specify the expectation and response for this request
+
+         $httpBackend.expectPOST('/add-msg.py', 'message content').respond(201, '');
+         $rootScope.saveMessage('message content');
+         expect($rootScope.status).toBe('Saving...');
+         $httpBackend.flush();
+         expect($rootScope.status).toBe('');
+       });
+
+
+       it('should send auth header', function() {
+         var controller = createController();
+         $httpBackend.flush();
+
+         $httpBackend.expectPOST('/add-msg.py', undefined, function(headers) {
+           // check if the header was send, if it wasn't the expectation won't
+           // match the request and the test will fail
+           return headers['Authorization'] == 'xxx';
+         }).respond(201, '');
+
+         $rootScope.saveMessage('whatever');
+         $httpBackend.flush();
+       });
+    });
+   
+ */ +angular.mock.$HttpBackendProvider = function() { + this.$get = ['$rootScope', createHttpBackendMock]; +}; + +/** + * General factory function for $httpBackend mock. + * Returns instance for unit testing (when no arguments specified): + * - passing through is disabled + * - auto flushing is disabled + * + * Returns instance for e2e testing (when `$delegate` and `$browser` specified): + * - passing through (delegating request to real backend) is enabled + * - auto flushing is enabled + * + * @param {Object=} $delegate Real $httpBackend instance (allow passing through if specified) + * @param {Object=} $browser Auto-flushing enabled if specified + * @return {Object} Instance of $httpBackend mock + */ +function createHttpBackendMock($rootScope, $delegate, $browser) { + var definitions = [], + expectations = [], + responses = [], + responsesPush = angular.bind(responses, responses.push), + copy = angular.copy; + + function createResponse(status, data, headers) { + if (angular.isFunction(status)) return status; + + return function() { + return angular.isNumber(status) + ? [status, data, headers] + : [200, status, data]; + }; + } + + // TODO(vojta): change params to: method, url, data, headers, callback + function $httpBackend(method, url, data, callback, headers, timeout, withCredentials) { + var xhr = new MockXhr(), + expectation = expectations[0], + wasExpected = false; + + function prettyPrint(data) { + return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp) + ? data + : angular.toJson(data); + } + + function wrapResponse(wrapped) { + if (!$browser && timeout && timeout.then) timeout.then(handleTimeout); + + return handleResponse; + + function handleResponse() { + var response = wrapped.response(method, url, data, headers); + xhr.$$respHeaders = response[2]; + callback(copy(response[0]), copy(response[1]), xhr.getAllResponseHeaders()); + } + + function handleTimeout() { + for (var i = 0, ii = responses.length; i < ii; i++) { + if (responses[i] === handleResponse) { + responses.splice(i, 1); + callback(-1, undefined, ''); + break; + } + } + } + } + + if (expectation && expectation.match(method, url)) { + if (!expectation.matchData(data)) + throw new Error('Expected ' + expectation + ' with different data\n' + + 'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data); + + if (!expectation.matchHeaders(headers)) + throw new Error('Expected ' + expectation + ' with different headers\n' + + 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' + + prettyPrint(headers)); + + expectations.shift(); + + if (expectation.response) { + responses.push(wrapResponse(expectation)); + return; + } + wasExpected = true; + } + + var i = -1, definition; + while ((definition = definitions[++i])) { + if (definition.match(method, url, data, headers || {})) { + if (definition.response) { + // if $browser specified, we do auto flush all requests + ($browser ? $browser.defer : responsesPush)(wrapResponse(definition)); + } else if (definition.passThrough) { + $delegate(method, url, data, callback, headers, timeout, withCredentials); + } else throw new Error('No response defined !'); + return; + } + } + throw wasExpected ? + new Error('No response defined !') : + new Error('Unexpected request: ' + method + ' ' + url + '\n' + + (expectation ? 'Expected ' + expectation : 'No more request expected')); + } + + /** + * @ngdoc method + * @name ngMock.$httpBackend#when + * @methodOf ngMock.$httpBackend + * @description + * Creates a new backend definition. + * + * @param {string} method HTTP method. + * @param {string|RegExp} url HTTP url. + * @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 controls how a matched + * request is handled. + * + * - respond – + * `{function([status,] data[, headers])|function(function(method, url, data, headers)}` + * – The respond method takes a set of static data to be returned or a function that can return + * an array containing response status (number), response data (string) and response headers + * (Object). + */ + $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); + } + }; + + if ($browser) { + chain.passThrough = function() { + definition.passThrough = true; + }; + } + + definitions.push(definition); + return chain; + }; + + /** + * @ngdoc method + * @name ngMock.$httpBackend#whenGET + * @methodOf ngMock.$httpBackend + * @description + * Creates a new backend definition for GET requests. For more info see `when()`. + * + * @param {string|RegExp} url HTTP url. + * @param {(Object|function(Object))=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * request is handled. + */ + + /** + * @ngdoc method + * @name ngMock.$httpBackend#whenHEAD + * @methodOf ngMock.$httpBackend + * @description + * Creates a new backend definition for HEAD requests. For more info see `when()`. + * + * @param {string|RegExp} url HTTP url. + * @param {(Object|function(Object))=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * request is handled. + */ + + /** + * @ngdoc method + * @name ngMock.$httpBackend#whenDELETE + * @methodOf ngMock.$httpBackend + * @description + * Creates a new backend definition for DELETE requests. For more info see `when()`. + * + * @param {string|RegExp} url HTTP url. + * @param {(Object|function(Object))=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * request is handled. + */ + + /** + * @ngdoc method + * @name ngMock.$httpBackend#whenPOST + * @methodOf ngMock.$httpBackend + * @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))=} 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. + */ + + /** + * @ngdoc method + * @name ngMock.$httpBackend#whenPUT + * @methodOf ngMock.$httpBackend + * @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))=} 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. + */ + + /** + * @ngdoc method + * @name ngMock.$httpBackend#whenJSONP + * @methodOf ngMock.$httpBackend + * @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. + */ + createShortMethods('when'); + + + /** + * @ngdoc method + * @name ngMock.$httpBackend#expect + * @methodOf ngMock.$httpBackend + * @description + * Creates a new request expectation. + * + * @param {string} method HTTP method. + * @param {string|RegExp} url HTTP url. + * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that + * receives data string and returns true if the data is as expected, or Object if request body + * is in JSON format. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current expectation. + * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * request is handled. + * + * - respond – + * `{function([status,] data[, headers])|function(function(method, url, data, headers)}` + * – The respond method takes a set of static data to be returned or a function that can return + * an array containing response status (number), response data (string) and response headers + * (Object). + */ + $httpBackend.expect = function(method, url, data, headers) { + var expectation = new MockHttpExpectation(method, url, data, headers); + expectations.push(expectation); + return { + respond: function(status, data, headers) { + expectation.response = createResponse(status, data, headers); + } + }; + }; + + + /** + * @ngdoc method + * @name ngMock.$httpBackend#expectGET + * @methodOf ngMock.$httpBackend + * @description + * Creates a new request expectation for GET requests. For more info see `expect()`. + * + * @param {string|RegExp} url HTTP url. + * @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. + */ + + /** + * @ngdoc method + * @name ngMock.$httpBackend#expectHEAD + * @methodOf ngMock.$httpBackend + * @description + * Creates a new request expectation for HEAD requests. For more info see `expect()`. + * + * @param {string|RegExp} url HTTP url. + * @param {Object=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * request is handled. + */ + + /** + * @ngdoc method + * @name ngMock.$httpBackend#expectDELETE + * @methodOf ngMock.$httpBackend + * @description + * Creates a new request expectation for DELETE requests. For more info see `expect()`. + * + * @param {string|RegExp} url HTTP url. + * @param {Object=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * request is handled. + */ + + /** + * @ngdoc method + * @name ngMock.$httpBackend#expectPOST + * @methodOf ngMock.$httpBackend + * @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)|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. + */ + + /** + * @ngdoc method + * @name ngMock.$httpBackend#expectPUT + * @methodOf ngMock.$httpBackend + * @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)|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. + */ + + /** + * @ngdoc method + * @name ngMock.$httpBackend#expectPATCH + * @methodOf ngMock.$httpBackend + * @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)|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. + */ + + /** + * @ngdoc method + * @name ngMock.$httpBackend#expectJSONP + * @methodOf ngMock.$httpBackend + * @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. + */ + createShortMethods('expect'); + + + /** + * @ngdoc method + * @name ngMock.$httpBackend#flush + * @methodOf ngMock.$httpBackend + * @description + * Flushes all pending requests using the trained responses. + * + * @param {number=} count Number of responses to flush (in the order they arrived). If undefined, + * 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 new Error('No pending request to flush !'); + + if (angular.isDefined(count)) { + while (count--) { + if (!responses.length) throw new Error('No more pending request to flush !'); + responses.shift()(); + } + } else { + while (responses.length) { + responses.shift()(); + } + } + $httpBackend.verifyNoOutstandingExpectation(); + }; + + + /** + * @ngdoc method + * @name ngMock.$httpBackend#verifyNoOutstandingExpectation + * @methodOf ngMock.$httpBackend + * @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. + * + * Typically, you would call this method following each test case that asserts requests using an + * "afterEach" clause. + * + *
+   *   afterEach($httpBackend.verifyNoOutstandingExpectation);
+   * 
+ */ + $httpBackend.verifyNoOutstandingExpectation = function() { + $rootScope.$digest(); + if (expectations.length) { + throw new Error('Unsatisfied requests: ' + expectations.join(', ')); + } + }; + + + /** + * @ngdoc method + * @name ngMock.$httpBackend#verifyNoOutstandingRequest + * @methodOf ngMock.$httpBackend + * @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. + * + *
+   *   afterEach($httpBackend.verifyNoOutstandingRequest);
+   * 
+ */ + $httpBackend.verifyNoOutstandingRequest = function() { + if (responses.length) { + throw new Error('Unflushed requests: ' + responses.length); + } + }; + + + /** + * @ngdoc method + * @name ngMock.$httpBackend#resetExpectations + * @methodOf ngMock.$httpBackend + * @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 + * $httpBackend mock. + */ + $httpBackend.resetExpectations = function() { + expectations.length = 0; + responses.length = 0; + }; + + return $httpBackend; + + + function createShortMethods(prefix) { + angular.forEach(['GET', 'DELETE', 'JSONP'], function(method) { + $httpBackend[prefix + method] = function(url, 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); + }; + }); + } +} + +function MockHttpExpectation(method, url, data, headers) { + + this.data = data; + this.headers = headers; + + this.match = function(m, u, d, h) { + if (method != m) return false; + if (!this.matchUrl(u)) return false; + if (angular.isDefined(d) && !this.matchData(d)) return false; + if (angular.isDefined(h) && !this.matchHeaders(h)) return false; + return true; + }; + + this.matchUrl = function(u) { + if (!url) return true; + if (angular.isFunction(url.test)) return url.test(u); + return url == u; + }; + + this.matchHeaders = function(h) { + if (angular.isUndefined(headers)) return true; + if (angular.isFunction(headers)) return headers(h); + return angular.equals(headers, h); + }; + + this.matchData = function(d) { + 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.equals(data, angular.fromJson(d)); + return data == d; + }; + + this.toString = function() { + return method + ' ' + url; + }; +} + +function MockXhr() { + + // hack for testing $http, $httpBackend + MockXhr.$$lastInstance = this; + + this.open = function(method, url, async) { + this.$$method = method; + this.$$url = url; + this.$$async = async; + this.$$reqHeaders = {}; + this.$$respHeaders = {}; + }; + + this.send = function(data) { + this.$$data = data; + }; + + this.setRequestHeader = function(key, value) { + this.$$reqHeaders[key] = value; + }; + + this.getResponseHeader = function(name) { + // 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; + + name = angular.lowercase(name); + header = this.$$respHeaders[name]; + if (header) return header; + + header = undefined; + angular.forEach(this.$$respHeaders, function(headerVal, headerName) { + if (!header && angular.lowercase(headerName) == name) header = headerVal; + }); + return header; + }; + + this.getAllResponseHeaders = function() { + var lines = []; + + angular.forEach(this.$$respHeaders, function(value, key) { + lines.push(key + ': ' + value); + }); + return lines.join('\n'); + }; + + this.abort = angular.noop; +} + + +/** + * @ngdoc function + * @name ngMock.$timeout + * @description + * + * This service is just a simple decorator for {@link ng.$timeout $timeout} service + * that adds a "flush" and "verifyNoPendingTasks" methods. + */ + +angular.mock.$TimeoutDecorator = function($delegate, $browser) { + + /** + * @ngdoc method + * @name ngMock.$timeout#flush + * @methodOf ngMock.$timeout + * @description + * + * Flushes the queue of pending tasks. + * + * @param {number=} delay maximum timeout amount to flush up until + */ + $delegate.flush = function(delay) { + $browser.defer.flush(delay); + }; + + /** + * @ngdoc method + * @name ngMock.$timeout#verifyNoPendingTasks + * @methodOf ngMock.$timeout + * @description + * + * Verifies that there are no pending tasks that need to be flushed. + */ + $delegate.verifyNoPendingTasks = function() { + if ($browser.deferredFns.length) { + throw new Error('Deferred tasks to flush (' + $browser.deferredFns.length + '): ' + + formatPendingTasksAsString($browser.deferredFns)); + } + }; + + function formatPendingTasksAsString(tasks) { + var result = []; + angular.forEach(tasks, function(task) { + result.push('{id: ' + task.id + ', ' + 'time: ' + task.time + '}'); + }); + + return result.join(', '); + } + + return $delegate; +}; + +/** + * + */ +angular.mock.$RootElementProvider = function() { + this.$get = function() { + return angular.element('
'); + }; +}; + +/** + * @ngdoc overview + * @name ngMock + * @description + * + * # ngMock + * + * The `ngMock` module providers 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. + * + * {@installModule mocks} + * + *
+ * + */ +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(['$provide', function($provide) { + $provide.decorator('$timeout', angular.mock.$TimeoutDecorator); +}]); + +/** + * @ngdoc overview + * @name ngMockE2E + * @description + * + * The `ngMockE2E` is an angular module which contains mocks suitable for end-to-end testing. + * Currently there is only one mock present in this module - + * the {@link ngMockE2E.$httpBackend e2e $httpBackend} mock. + */ +angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { + $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator); +}]); + +/** + * @ngdoc object + * @name ngMockE2E.$httpBackend + * @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}. + * + * *Note*: For fake http backend implementation suitable for unit testing please see + * {@link ngMock.$httpBackend unit-testing $httpBackend mock}. + * + * This implementation can be used to respond with static or dynamic responses via the `when` api + * and its shortcuts (`whenGET`, `whenPOST`, etc) and optionally pass through requests to the + * real $httpBackend for specific requests (e.g. to interact with certain remote apis or to fetch + * templates from a webserver). + * + * As opposed to unit-testing, in an end-to-end testing scenario or in scenario when an application + * is being developed with the real backend api replaced with a mock, it is often desirable for + * certain category of requests to bypass the mock and issue a real http request (e.g. to fetch + * templates or static files from the webserver). To configure the backend with this behavior + * 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 + * 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: + * + *
+ *   myAppDev = angular.module('myAppDev', ['myApp', 'ngMockE2E']);
+ *   myAppDev.run(function($httpBackend) {
+ *     phones = [{name: 'phone1'}, {name: 'phone2'}];
+ *
+ *     // returns the current list of phones
+ *     $httpBackend.whenGET('/phones').respond(phones);
+ *
+ *     // adds a new phone to the phones array
+ *     $httpBackend.whenPOST('/phones').respond(function(method, url, data) {
+ *       phones.push(angular.fromJson(data));
+ *     });
+ *     $httpBackend.whenGET(/^\/templates\//).passThrough();
+ *     //...
+ *   });
+ * 
+ * + * Afterwards, bootstrap your app with this new module. + */ + +/** + * @ngdoc method + * @name ngMockE2E.$httpBackend#when + * @methodOf ngMockE2E.$httpBackend + * @description + * Creates a new backend definition. + * + * @param {string} method HTTP method. + * @param {string|RegExp} url HTTP url. + * @param {(string|RegExp)=} data HTTP request body. + * @param {(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. + * + * - 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). + * - 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. + */ + +/** + * @ngdoc method + * @name ngMockE2E.$httpBackend#whenGET + * @methodOf ngMockE2E.$httpBackend + * @description + * Creates a new backend definition for GET requests. For more info see `when()`. + * + * @param {string|RegExp} url HTTP url. + * @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. + */ + +/** + * @ngdoc method + * @name ngMockE2E.$httpBackend#whenHEAD + * @methodOf ngMockE2E.$httpBackend + * @description + * Creates a new backend definition for HEAD requests. For more info see `when()`. + * + * @param {string|RegExp} url HTTP url. + * @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. + */ + +/** + * @ngdoc method + * @name ngMockE2E.$httpBackend#whenDELETE + * @methodOf ngMockE2E.$httpBackend + * @description + * Creates a new backend definition for DELETE requests. For more info see `when()`. + * + * @param {string|RegExp} url HTTP url. + * @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. + */ + +/** + * @ngdoc method + * @name ngMockE2E.$httpBackend#whenPOST + * @methodOf ngMockE2E.$httpBackend + * @description + * Creates a new backend definition for POST requests. For more info see `when()`. + * + * @param {string|RegExp} url HTTP url. + * @param {(string|RegExp)=} data HTTP request body. + * @param {(Object|function(Object))=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. + */ + +/** + * @ngdoc method + * @name ngMockE2E.$httpBackend#whenPUT + * @methodOf ngMockE2E.$httpBackend + * @description + * Creates a new backend definition for PUT requests. For more info see `when()`. + * + * @param {string|RegExp} url HTTP url. + * @param {(string|RegExp)=} data HTTP request body. + * @param {(Object|function(Object))=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. + */ + +/** + * @ngdoc method + * @name ngMockE2E.$httpBackend#whenPATCH + * @methodOf ngMockE2E.$httpBackend + * @description + * Creates a new backend definition for PATCH requests. For more info see `when()`. + * + * @param {string|RegExp} url HTTP url. + * @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. + */ + +/** + * @ngdoc method + * @name ngMockE2E.$httpBackend#whenJSONP + * @methodOf ngMockE2E.$httpBackend + * @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` and `passThrough` methods that + * control how a matched request is handled. + */ +angular.mock.e2e = {}; +angular.mock.e2e.$httpBackendDecorator = + ['$rootScope', '$delegate', '$browser', createHttpBackendMock]; + + +angular.mock.clearDataCache = function() { + var key, + cache = angular.element.cache; + + for(key in cache) { + if (Object.prototype.hasOwnProperty.call(cache,key)) { + var handle = cache[key].handle; + + handle && angular.element(handle.elem).off(); + delete cache[key]; + } + } +}; + + + +if(window.jasmine || window.mocha) { + + var currentSpec = null, + isSpecRunning = function() { + return currentSpec && (window.mocha || currentSpec.queue.running); + }; + + + beforeEach(function() { + currentSpec = this; + }); + + afterEach(function() { + var injector = currentSpec.$injector; + + currentSpec.$injector = null; + currentSpec.$modules = null; + currentSpec = null; + + if (injector) { + injector.get('$rootElement').off(); + 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]; + }); + + MockXhr.$$lastInstance = null; + + angular.forEach(angular.callbacks, function(val, key) { + delete angular.callbacks[key]; + }); + angular.callbacks.counter = 0; + }); + + /** + * @ngdoc function + * @name angular.mock.module + * @description + * + * *NOTE*: This function is also published on window for easy access.
+ * + * This function registers a module configuration code. It collects the configuration information + * which will be used when the injector is created by {@link angular.mock.inject inject}. + * + * See {@link angular.mock.inject inject} for usage example + * + * @param {...(string|Function|Object)} fns any number of modules which are represented as string + * aliases or as anonymous module initialization functions. The modules are used to + * configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. If an + * object literal is passed they will be register as values in the module, the key being + * the module name and the value being what is returned. + */ + window.module = angular.mock.module = function() { + var moduleFns = Array.prototype.slice.call(arguments, 0); + return isSpecRunning() ? workFn() : workFn; + ///////////////////// + function workFn() { + if (currentSpec.$injector) { + throw new Error('Injector already created, can not register a module!'); + } else { + var modules = currentSpec.$modules || (currentSpec.$modules = []); + angular.forEach(moduleFns, function(module) { + if (angular.isObject(module) && !angular.isArray(module)) { + modules.push(function($provide) { + angular.forEach(module, function(value, key) { + $provide.value(key, value); + }); + }); + } else { + modules.push(module); + } + }); + } + } + }; + + /** + * @ngdoc function + * @name angular.mock.inject + * @description + * + * *NOTE*: This function is also published on window for easy access.
+ * + * The inject function wraps a function into an injectable function. The inject() creates new + * instance of {@link AUTO.$injector $injector} per test, which is then used for + * resolving references. + * + * + * ## Resolving References (Underscore Wrapping) + * Often, we would like to inject a reference once, in a `beforeEach()` block and reuse this + * in multiple `it()` clauses. To be able to do this we must assign the reference to a variable + * that is declared in the scope of the `describe()` block. Since we would, most likely, want + * the variable to have the same name of the reference we have a problem, since the parameter + * to the `inject()` function would hide the outer variable. + * + * To help with this, the injected parameters can, optionally, be enclosed with underscores. + * These are ignored by the injector when the reference name is resolved. + * + * For example, the parameter `_myService_` would be resolved as the reference `myService`. + * Since it is available in the function body as _myService_, we can then assign it to a variable + * defined in an outer scope. + * + * ``` + * // Defined out reference variable outside + * var myService; + * + * // Wrap the parameter in underscores + * beforeEach( inject( function(_myService_){ + * myService = _myService_; + * })); + * + * // Use myService in a series of tests. + * it('makes use of myService', function() { + * myService.doStuff(); + * }); + * + * ``` + * + * See also {@link angular.mock.module angular.mock.module} + * + * ## Example + * Example of what a typical jasmine tests looks like with the inject method. + *
+   *
+   *   angular.module('myApplicationModule', [])
+   *       .value('mode', 'app')
+   *       .value('version', 'v1.0.1');
+   *
+   *
+   *   describe('MyApp', function() {
+   *
+   *     // You need to load modules that you want to test,
+   *     // it loads only the "ng" module by default.
+   *     beforeEach(module('myApplicationModule'));
+   *
+   *
+   *     // inject() is used to inject arguments of all given functions
+   *     it('should provide a version', inject(function(mode, version) {
+   *       expect(version).toEqual('v1.0.1');
+   *       expect(mode).toEqual('app');
+   *     }));
+   *
+   *
+   *     // The inject and module method can also be used inside of the it or beforeEach
+   *     it('should override a version and test the new version is injected', function() {
+   *       // module() takes functions or strings (module aliases)
+   *       module(function($provide) {
+   *         $provide.value('version', 'overridden'); // override version here
+   *       });
+   *
+   *       inject(function(version) {
+   *         expect(version).toEqual('overridden');
+   *       });
+   *     });
+   *   });
+   *
+   * 
+ * + * @param {...Function} fns any number of functions which will be injected using the injector. + */ + window.inject = angular.mock.inject = function() { + var blockFns = Array.prototype.slice.call(arguments, 0); + var errorForStack = new Error('Declaration Location'); + return isSpecRunning() ? workFn() : workFn; + ///////////////////// + function workFn() { + var modules = currentSpec.$modules || []; + + modules.unshift('ngMock'); + modules.unshift('ng'); + var injector = currentSpec.$injector; + if (!injector) { + injector = currentSpec.$injector = angular.injector(modules); + } + for(var i = 0, ii = blockFns.length; i < ii; i++) { + try { + /* jshint -W040 *//* Jasmine explicitly provides a `this` object when calling functions */ + injector.invoke(blockFns[i] || angular.noop, this); + /* jshint +W040 */ + } catch (e) { + if(e.stack && errorForStack) e.stack += '\n' + errorForStack.stack; + throw e; + } finally { + errorForStack = null; + } + } + } + }; +} + + +})(window, window.angular); diff --git a/test/unit/karma.conf.js b/test/unit/karma.conf.js new file mode 100644 index 000000000..00f7b04d2 --- /dev/null +++ b/test/unit/karma.conf.js @@ -0,0 +1,56 @@ +// base path, that will be used to resolve files and exclude +basePath = '../..'; + +// list of files / patterns to load in the browser +files = [ + JASMINE, + JASMINE_ADAPTER, + 'assets/js/jquery-1.11.1.min.js', + 'assets/js/angularjs/1.2.6/angular.min.js', + 'assets/js/angularjs/1.2.6/angular-route.min.js', + 'assets/js/angularjs/1.2.6/angular-resource.min.js', + 'test/assets/angular/angular-mocks.js', + 'app/**/*.js', + 'test/unit/**/*.spec.js', + 'dist/templates/**/*.js' +]; + +// use dots reporter, as travis terminal does not support escaping sequences +// possible values: 'dots' || 'progress' +reporters = 'progress'; + +// these are default values, just to show available options + +// web server port +port = 8089; + +// cli runner port +runnerPort = 9109; + +urlRoot = '/__test/'; + +// enable / disable colors in the output (reporters and logs) +colors = true; + +// level of logging +// possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG +logLevel = LOG_INFO; + +// enable / disable watching file and executing tests whenever any file changes +autoWatch = false; + +// polling interval in ms (ignored on OS that support inotify) +autoWatchInterval = 0; + +// Start these browsers, currently available: +// - Chrome +// - ChromeCanary +// - Firefox +// - Opera +// - Safari +// - PhantomJS +browsers = ['Chrome']; + +// Continuous Integration mode +// if true, it capture browsers, run tests and exit +singleRun = true; \ No newline at end of file From 17847190473f86172437e505a2d41d365641516a Mon Sep 17 00:00:00 2001 From: Kevan Ahlquist Date: Sun, 4 Jan 2015 20:35:47 -0600 Subject: [PATCH 4/5] Added unit tests for filters. --- app/shared/filters.js | 6 +- app/shared/services.js | 2 +- index.html | 2 +- test/unit/shared/filters.spec.js | 156 +++++++++++++++++++++++++++++++ 4 files changed, 161 insertions(+), 5 deletions(-) create mode 100644 test/unit/shared/filters.spec.js diff --git a/app/shared/filters.js b/app/shared/filters.js index 2efe3b784..e6becfdb8 100644 --- a/app/shared/filters.js +++ b/app/shared/filters.js @@ -1,4 +1,4 @@ -angular.module('dockerui.filters', []) +angular.module('<%= pkg.name %>.filters', []) .filter('truncate', function() { 'use strict'; return function(text, length, end) { @@ -7,14 +7,14 @@ angular.module('dockerui.filters', []) } if (end === undefined){ - end = "..."; + end = '...'; } if (text.length <= length || text.length - end.length <= length) { return text; } else { - return String(text).substring(0, length-end.length) + end; + return String(text).substring(0, length - end.length) + end; } }; }) diff --git a/app/shared/services.js b/app/shared/services.js index 5a06bc5db..d825f94fd 100644 --- a/app/shared/services.js +++ b/app/shared/services.js @@ -1,4 +1,4 @@ -angular.module('dockerui.services', ['ngResource']) +angular.module('<%= pkg.name %>.services', ['ngResource']) .factory('Container', function($resource, Settings) { 'use strict'; // Resource for interacting with the docker containers diff --git a/index.html b/index.html index fb79d52d4..89e49bfc9 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,5 @@ - + DockerUI diff --git a/test/unit/shared/filters.spec.js b/test/unit/shared/filters.spec.js new file mode 100644 index 000000000..d83f37911 --- /dev/null +++ b/test/unit/shared/filters.spec.js @@ -0,0 +1,156 @@ +describe('filters', function () { + beforeEach(module('<%= pkg.name %>.filters')); + + describe('truncate', function () { + it('should truncate the string to 10 characters ending in "..." by default', inject(function(truncateFilter) { + expect(truncateFilter('this is 20 chars long')).toBe('this is...'); + })); + + it('should truncate the string to 7 characters ending in "..."', inject(function(truncateFilter) { + expect(truncateFilter('this is 20 chars long', 7)).toBe('this...'); + })); + + it('should truncate the string to 10 characters ending in "???"', inject(function(truncateFilter) { + expect(truncateFilter('this is 20 chars long', 10, '???')).toBe('this is???'); + })); + }); + + describe('statusbadge', function () { + it('should be "important" when input is "Ghost"', inject(function(statusbadgeFilter) { + expect(statusbadgeFilter('Ghost')).toBe('important'); + })); + + it('should be "success" when input is "Exit 0"', inject(function(statusbadgeFilter) { + expect(statusbadgeFilter('Exit 0')).toBe('success'); + })); + + it('should be "warning" when exit code is non-zero', inject(function(statusbadgeFilter) { + expect(statusbadgeFilter('Exit 1')).toBe('warning'); + })); + }); + + describe('getstatetext', function () { + + it('should return an empty string when state is undefined', inject(function(getstatetextFilter) { + expect(getstatetextFilter(undefined)).toBe(''); + })); + + it('should detect a Ghost state', inject(function(getstatetextFilter) { + var state = { + Ghost: true, + Running: true, + Paused: false + }; + expect(getstatetextFilter(state)).toBe('Ghost'); + })); + + it('should detect a Paused state', inject(function(getstatetextFilter) { + var state = { + Ghost: false, + Running: true, + Paused: true + }; + expect(getstatetextFilter(state)).toBe('Running (Paused)'); + })); + + it('should detect a Running state', inject(function(getstatetextFilter) { + var state = { + Ghost: false, + Running: true, + Paused: false + }; + expect(getstatetextFilter(state)).toBe('Running'); + })); + + it('should detect a Stopped state', inject(function(getstatetextFilter) { + var state = { + Ghost: false, + Running: false, + Paused: false + }; + expect(getstatetextFilter(state)).toBe('Stopped'); + })); + }); + + describe('getstatelabel', function () { + it('should return an empty string when state is undefined', inject(function(getstatelabelFilter) { + expect(getstatelabelFilter(undefined)).toBe(''); + })); + + it('should return label-important when a ghost state is detected', inject(function(getstatelabelFilter) { + var state = { + Ghost: true, + Running: true, + Paused: false + }; + expect(getstatelabelFilter(state)).toBe('label-important'); + })); + + it('should return label-success when a running state is detected', inject(function(getstatelabelFilter) { + var state = { + Ghost: false, + Running: true, + Paused: false + }; + expect(getstatelabelFilter(state)).toBe('label-success'); + })); + }); + + describe('humansize', function () { + it('should return n/a when size is zero', inject(function(humansizeFilter) { + expect(humansizeFilter(0)).toBe('n/a'); + })); + + it('should handle Bytes values', inject(function(humansizeFilter) { + expect(humansizeFilter(512)).toBe('512 Bytes'); + })); + + it('should handle KB values', inject(function(humansizeFilter) { + expect(humansizeFilter(5120)).toBe('5 KB'); + })); + + it('should handle MB values', inject(function(humansizeFilter) { + expect(humansizeFilter(5 * Math.pow(10, 6))).toBe('5 MB'); + })); + + it('should handle GB values', inject(function(humansizeFilter) { + expect(humansizeFilter(5 * Math.pow(10, 9))).toBe('5 GB'); + })); + + it('should handle TB values', inject(function(humansizeFilter) { + expect(humansizeFilter(5 * Math.pow(10, 12))).toBe('5 TB'); + })); + }); + + describe('containername', function () { + it('should strip the leading slash from container name', inject(function(containernameFilter) { + var container = { + Names: ['/elegant_ardinghelli'] + }; + + expect(containernameFilter(container)).toBe('elegant_ardinghelli'); + })); + }); + + describe('repotag', function () { + it('should not display empty repo tag', inject(function(repotagFilter) { + var image = { + RepoTags: [':'] + }; + expect(repotagFilter(image)).toBe(''); + })); + + it('should display a normal repo tag', inject(function(repotagFilter) { + var image = { + RepoTags: ['ubuntu:latest'] + }; + expect(repotagFilter(image)).toBe('ubuntu:latest'); + })); + }); + + describe('getdate', function () { + it('should convert the Docker date to a human readable form', inject(function(getdateFilter) { + expect(getdateFilter(1420424998)).toBe('Sun Jan 04 2015'); + })); + }); +}); \ No newline at end of file From 190087e6cac5799ff1868a9cc8902f50fe5c8524 Mon Sep 17 00:00:00 2001 From: Kevan Ahlquist Date: Wed, 14 Jan 2015 00:25:25 -0600 Subject: [PATCH 5/5] Added install and test targets to Makefile --- Makefile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Makefile b/Makefile index 0676b2ae3..bbca93f01 100644 --- a/Makefile +++ b/Makefile @@ -5,10 +5,16 @@ OPEN = $(shell which xdg-open || which open) PORT ?= 9000 +install: + npm install -g grunt-cli + build: grunt build docker build --rm -t dockerui . +test: + grunt + run: -docker stop dockerui -docker rm dockerui