├── .gitignore ├── .jshintrc ├── .travis.yml ├── Gruntfile.js ├── LICENSE ├── README.md ├── build ├── ng-socket-io.js └── ng-socket-io.min.js ├── karma.conf.js ├── package.json ├── src └── ng-socket-io.js └── test ├── lib ├── angular-mocks.js └── angular.js └── ng-socket-io.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | *.log -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "strict": true, 3 | "curly": true, 4 | "eqeqeq": true, 5 | "immed": true, 6 | "noempty": true, 7 | "noarg": true, 8 | "quotmark": "single", 9 | "undef": true, 10 | "eqnull": true, 11 | "globals": { 12 | "angular": true, 13 | "module": true, 14 | "inject": true, 15 | "beforeEach": true, 16 | "describe": true, 17 | "it": true, 18 | "expect": true, 19 | "jasmine": true, 20 | "spyOn": true, 21 | "io": true 22 | } 23 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_script: 5 | - npm install -g grunt-cli karma -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 'use strict'; 3 | 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | files: { 7 | js: { 8 | src: 'src/ng-socket-io.js', 9 | out: 'build/<%= pkg.name %>.js', 10 | outMin: 'build/<%= pkg.name %>.min.js' 11 | }, 12 | spec: { 13 | src: 'test/ng-socket-io.spec.js' 14 | } 15 | }, 16 | // Validates the JS file with JSHint 17 | jshint: { 18 | files: ['Gruntfile.js', ['<%= files.js.src %>'], ['<%= files.spec.src %>']], 19 | options: { 20 | jshintrc: '.jshintrc' 21 | } 22 | }, 23 | // Runs all unit tests with Karma 24 | karma: { 25 | options: { 26 | configFile: 'karma.conf.js' 27 | }, 28 | continuous: { 29 | singleRun: true, 30 | browsers: ['PhantomJS'] 31 | } 32 | }, 33 | clean: { 34 | build: ['build/'] 35 | }, 36 | // Adds AngularJS dependency injection annotations 37 | ngAnnotate: { 38 | factories: { 39 | files: { 40 | '<%= files.js.out %>': ['<%= files.js.src %>'] 41 | } 42 | } 43 | }, 44 | // Minifies the JS file 45 | uglify: { 46 | build: { 47 | files: { 48 | '<%= files.js.outMin %>': ['<%= files.js.out %>'] 49 | } 50 | } 51 | } 52 | }); 53 | 54 | grunt.loadNpmTasks('grunt-contrib-uglify'); 55 | grunt.loadNpmTasks('grunt-contrib-jshint'); 56 | grunt.loadNpmTasks('grunt-contrib-clean'); 57 | grunt.loadNpmTasks('grunt-ng-annotate'); 58 | grunt.loadNpmTasks('grunt-karma'); 59 | 60 | grunt.registerTask('test', [ 61 | 'jshint', 62 | 'karma' 63 | ]); 64 | grunt.registerTask('default', [ 65 | 'jshint', 66 | 'karma', 67 | 'clean', 68 | 'ngAnnotate', 69 | 'uglify' 70 | ]); 71 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Michael Benford 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ngSocketIO 2 | =========== 3 | [![Build Status](https://travis-ci.org/mbenford/ngSocketIO.png?branch=master)](https://travis-ci.org/mbenford/ngSocketIO) 4 | 5 | Socket.IO module for AngularJS 6 | 7 | ## Requirements 8 | 9 | - AngularJS 1.0.5+ 10 | - Socket.IO 0.9.16 11 | 12 | ## Installing 13 | 14 | Simply download either `ng-socket-io` or `ng-socket-io.min.js` from the build folder and add it to your web application. Just make sure it's included after the AngularJS script. 15 | 16 | ## Usage 17 | 18 | 1. Add the `socket-io` module as a dependency in your AngularJS app; 19 | 2. Inject the `socket` factory wherever you need to use Socket.IO; 20 | 3. You're done! 21 | 22 | ## Example 23 | 24 | 25 | 26 | 40 | 41 | ## Cancelling a subscription automatically on scope destruction 42 | 43 | If you want to unsubscribe from an event automatically on scope destruction, just call `bindTo` passing the current scope: 44 | 45 | socket.on('someEvent', function(data) { 46 | ... 47 | }).bindTo($scope); -------------------------------------------------------------------------------- /build/ng-socket-io.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('socket-io', []).factory('socket', ["$rootScope","io", function($rootScope, io) { 5 | var socket = io.connect(), 6 | events = {}, 7 | that = {}; 8 | 9 | var addCallback = function(name, callback) { 10 | var event = events[name], 11 | wrappedCallback = wrapCallback(callback); 12 | 13 | if (!event) { 14 | event = events[name] = []; 15 | } 16 | 17 | event.push({ callback: callback, wrapper: wrappedCallback }); 18 | return wrappedCallback; 19 | }; 20 | 21 | var removeCallback = function(name, callback) { 22 | var event = events[name], 23 | wrappedCallback; 24 | 25 | if (event) { 26 | for(var i = event.length - 1; i >= 0; i--) { 27 | if (event[i].callback === callback) { 28 | wrappedCallback = event[i].wrapper; 29 | event.slice(i, 1); 30 | break; 31 | } 32 | } 33 | } 34 | return wrappedCallback; 35 | }; 36 | 37 | var removeAllCallbacks = function(name) { 38 | delete events[name]; 39 | }; 40 | 41 | var wrapCallback = function(callback) { 42 | var wrappedCallback = angular.noop; 43 | 44 | if (callback) { 45 | wrappedCallback = function() { 46 | var args = arguments; 47 | $rootScope.$apply(function() { 48 | callback.apply(socket, args); 49 | }); 50 | }; 51 | } 52 | return wrappedCallback; 53 | }; 54 | 55 | var listener = function(name, callback) { 56 | return { 57 | bindTo: function(scope) { 58 | if (scope != null) { 59 | scope.$on('$destroy', function() { 60 | that.removeListener(name, callback); 61 | }); 62 | } 63 | } 64 | }; 65 | }; 66 | 67 | that = { 68 | on: function(name, callback) { 69 | socket.on(name, addCallback(name, callback)); 70 | return listener(name, callback); 71 | }, 72 | once: function(name, callback) { 73 | socket.once(name, addCallback(name, callback)); 74 | return listener(name, callback); 75 | }, 76 | removeListener: function(name, callback) { 77 | socket.removeListener(name, removeCallback(name, callback)); 78 | }, 79 | removeAllListeners: function(name) { 80 | socket.removeAllListeners(name); 81 | removeAllCallbacks(name); 82 | }, 83 | emit: function(name, data, callback) { 84 | if (callback) { 85 | socket.emit(name, data, wrapCallback(callback)); 86 | } 87 | else { 88 | socket.emit(name, data); 89 | } 90 | } 91 | }; 92 | 93 | return that; 94 | }]) 95 | .factory('io', function() { 96 | return io; 97 | }); 98 | 99 | }()); -------------------------------------------------------------------------------- /build/ng-socket-io.min.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";angular.module("socket-io",[]).factory("socket",["$rootScope","io",function(a,b){var c=b.connect(),d={},e={},f=function(a,b){var c=d[a],e=i(b);return c||(c=d[a]=[]),c.push({callback:b,wrapper:e}),e},g=function(a,b){var c,e=d[a];if(e)for(var f=e.length-1;f>=0;f--)if(e[f].callback===b){c=e[f].wrapper,e.slice(f,1);break}return c},h=function(a){delete d[a]},i=function(b){var d=angular.noop;return b&&(d=function(){var d=arguments;a.$apply(function(){b.apply(c,d)})}),d},j=function(a,b){return{bindTo:function(c){null!=c&&c.$on("$destroy",function(){e.removeListener(a,b)})}}};return e={on:function(a,b){return c.on(a,f(a,b)),j(a,b)},once:function(a,b){return c.once(a,f(a,b)),j(a,b)},removeListener:function(a,b){c.removeListener(a,g(a,b))},removeAllListeners:function(a){c.removeAllListeners(a),h(a)},emit:function(a,b,d){d?c.emit(a,b,i(d)):c.emit(a,b)}}}]).factory("io",function(){return io})}(); -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | 4 | // base path, that will be used to resolve files and exclude 5 | basePath: '', 6 | 7 | 8 | // frameworks to use 9 | frameworks: ['jasmine'], 10 | 11 | 12 | // list of files / patterns to load in the browser 13 | files: [ 14 | 'test/lib/angular.js', 15 | 'test/lib/angular-mocks.js', 16 | 'test/ng-socket-io.spec.js', 17 | 'src/ng-socket-io.js' 18 | ], 19 | 20 | 21 | // list of files to exclude 22 | exclude: [ 23 | 24 | ], 25 | 26 | 27 | // test results reporter to use 28 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 29 | reporters: ['progress'], 30 | 31 | 32 | // web server port 33 | port: 9876, 34 | 35 | 36 | // enable / disable colors in the output (reporters and logs) 37 | colors: true, 38 | 39 | 40 | // level of logging 41 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 42 | logLevel: config.LOG_INFO, 43 | 44 | 45 | // enable / disable watching file and executing tests whenever any file changes 46 | autoWatch: true, 47 | 48 | 49 | // Start these browsers, currently available: 50 | // - Chrome 51 | // - ChromeCanary 52 | // - Firefox 53 | // - Opera (has to be installed with `npm install karma-opera-launcher`) 54 | // - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`) 55 | // - PhantomJS 56 | // - IE (only Windows; has to be installed with `npm install karma-ie-launcher`) 57 | browsers: ['Chrome'], 58 | 59 | 60 | // If browser does not capture in given timeout [ms], kill it 61 | captureTimeout: 60000, 62 | 63 | 64 | // Continuous Integration mode 65 | // if true, it capture browsers, run tests and exit 66 | singleRun: false 67 | }); 68 | }; 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "name": "Michael Benford", 4 | "email": "michael@michaelbenford.net" 5 | }, 6 | "name": "ng-socket-io", 7 | "version": "0.2.0", 8 | "description": "SocketIO module for AngularJS", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/mbenford/ngSocketIO.git" 13 | }, 14 | "scripts": { 15 | "test": "grunt test" 16 | }, 17 | "dependencies": {}, 18 | "devDependencies": { 19 | "grunt": "=0.4.1", 20 | "grunt-contrib-uglify": "=0.2.2", 21 | "grunt-contrib-watch": "=0.5.3", 22 | "grunt-contrib-jshint": "=0.6.3", 23 | "grunt-contrib-clean": "=0.5.0", 24 | "grunt-ng-annotate": "=0.0.4", 25 | "grunt-karma": "=0.6.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ng-socket-io.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('socket-io', []).factory('socket', function($rootScope, io) { 5 | var socket = io.connect(), 6 | events = {}, 7 | that = {}; 8 | 9 | var addCallback = function(name, callback) { 10 | var event = events[name], 11 | wrappedCallback = wrapCallback(callback); 12 | 13 | if (!event) { 14 | event = events[name] = []; 15 | } 16 | 17 | event.push({ callback: callback, wrapper: wrappedCallback }); 18 | return wrappedCallback; 19 | }; 20 | 21 | var removeCallback = function(name, callback) { 22 | var event = events[name], 23 | wrappedCallback; 24 | 25 | if (event) { 26 | for(var i = event.length - 1; i >= 0; i--) { 27 | if (event[i].callback === callback) { 28 | wrappedCallback = event[i].wrapper; 29 | event.slice(i, 1); 30 | break; 31 | } 32 | } 33 | } 34 | return wrappedCallback; 35 | }; 36 | 37 | var removeAllCallbacks = function(name) { 38 | delete events[name]; 39 | }; 40 | 41 | var wrapCallback = function(callback) { 42 | var wrappedCallback = angular.noop; 43 | 44 | if (callback) { 45 | wrappedCallback = function() { 46 | var args = arguments; 47 | $rootScope.$apply(function() { 48 | callback.apply(socket, args); 49 | }); 50 | }; 51 | } 52 | return wrappedCallback; 53 | }; 54 | 55 | var listener = function(name, callback) { 56 | return { 57 | bindTo: function(scope) { 58 | if (scope != null) { 59 | scope.$on('$destroy', function() { 60 | that.removeListener(name, callback); 61 | }); 62 | } 63 | } 64 | }; 65 | }; 66 | 67 | that = { 68 | on: function(name, callback) { 69 | socket.on(name, addCallback(name, callback)); 70 | return listener(name, callback); 71 | }, 72 | once: function(name, callback) { 73 | socket.once(name, addCallback(name, callback)); 74 | return listener(name, callback); 75 | }, 76 | removeListener: function(name, callback) { 77 | socket.removeListener(name, removeCallback(name, callback)); 78 | }, 79 | removeAllListeners: function(name) { 80 | socket.removeAllListeners(name); 81 | removeAllCallbacks(name); 82 | }, 83 | emit: function(name, data, callback) { 84 | if (callback) { 85 | socket.emit(name, data, wrapCallback(callback)); 86 | } 87 | else { 88 | socket.emit(name, data); 89 | } 90 | } 91 | }; 92 | 93 | return that; 94 | }) 95 | .factory('io', function() { 96 | return io; 97 | }); 98 | 99 | }()); -------------------------------------------------------------------------------- /test/lib/angular-mocks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @license AngularJS v1.2.0 5 | * (c) 2010-2012 Google, Inc. http://angularjs.org 6 | * License: MIT 7 | * 8 | * TODO(vojta): wrap whole file into closure during build 9 | */ 10 | 11 | /** 12 | * @ngdoc overview 13 | * @name angular.mock 14 | * @description 15 | * 16 | * Namespace from 'angular-mocks.js' which contains testing related code. 17 | */ 18 | angular.mock = {}; 19 | 20 | /** 21 | * ! This is a private undocumented service ! 22 | * 23 | * @name ngMock.$browser 24 | * 25 | * @description 26 | * This service is a mock implementation of {@link ng.$browser}. It provides fake 27 | * implementation for commonly used browser apis that are hard to test, e.g. setTimeout, xhr, 28 | * cookies, etc... 29 | * 30 | * The api of this service is the same as that of the real {@link ng.$browser $browser}, except 31 | * that there are several helper methods available which can be used in tests. 32 | */ 33 | angular.mock.$BrowserProvider = function() { 34 | this.$get = function() { 35 | return new angular.mock.$Browser(); 36 | }; 37 | }; 38 | 39 | angular.mock.$Browser = function() { 40 | var self = this; 41 | 42 | this.isMock = true; 43 | self.$$url = "http://server/"; 44 | self.$$lastUrl = self.$$url; // used by url polling fn 45 | self.pollFns = []; 46 | 47 | // TODO(vojta): remove this temporary api 48 | self.$$completeOutstandingRequest = angular.noop; 49 | self.$$incOutstandingRequestCount = angular.noop; 50 | 51 | 52 | // register url polling fn 53 | 54 | self.onUrlChange = function(listener) { 55 | self.pollFns.push( 56 | function() { 57 | if (self.$$lastUrl != self.$$url) { 58 | self.$$lastUrl = self.$$url; 59 | listener(self.$$url); 60 | } 61 | } 62 | ); 63 | 64 | return listener; 65 | }; 66 | 67 | self.cookieHash = {}; 68 | self.lastCookieHash = {}; 69 | self.deferredFns = []; 70 | self.deferredNextId = 0; 71 | 72 | self.defer = function(fn, delay) { 73 | delay = delay || 0; 74 | self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId}); 75 | self.deferredFns.sort(function(a,b){ return a.time - b.time;}); 76 | return self.deferredNextId++; 77 | }; 78 | 79 | 80 | /** 81 | * @name ngMock.$browser#defer.now 82 | * @propertyOf ngMock.$browser 83 | * 84 | * @description 85 | * Current milliseconds mock time. 86 | */ 87 | self.defer.now = 0; 88 | 89 | 90 | self.defer.cancel = function(deferId) { 91 | var fnIndex; 92 | 93 | angular.forEach(self.deferredFns, function(fn, index) { 94 | if (fn.id === deferId) fnIndex = index; 95 | }); 96 | 97 | if (fnIndex !== undefined) { 98 | self.deferredFns.splice(fnIndex, 1); 99 | return true; 100 | } 101 | 102 | return false; 103 | }; 104 | 105 | 106 | /** 107 | * @name ngMock.$browser#defer.flush 108 | * @methodOf ngMock.$browser 109 | * 110 | * @description 111 | * Flushes all pending requests and executes the defer callbacks. 112 | * 113 | * @param {number=} number of milliseconds to flush. See {@link #defer.now} 114 | */ 115 | self.defer.flush = function(delay) { 116 | if (angular.isDefined(delay)) { 117 | self.defer.now += delay; 118 | } else { 119 | if (self.deferredFns.length) { 120 | self.defer.now = self.deferredFns[self.deferredFns.length-1].time; 121 | } else { 122 | throw new Error('No deferred tasks to be flushed'); 123 | } 124 | } 125 | 126 | while (self.deferredFns.length && self.deferredFns[0].time <= self.defer.now) { 127 | self.deferredFns.shift().fn(); 128 | } 129 | }; 130 | 131 | self.$$baseHref = ''; 132 | self.baseHref = function() { 133 | return this.$$baseHref; 134 | }; 135 | }; 136 | angular.mock.$Browser.prototype = { 137 | 138 | /** 139 | * @name ngMock.$browser#poll 140 | * @methodOf ngMock.$browser 141 | * 142 | * @description 143 | * run all fns in pollFns 144 | */ 145 | poll: function poll() { 146 | angular.forEach(this.pollFns, function(pollFn){ 147 | pollFn(); 148 | }); 149 | }, 150 | 151 | addPollFn: function(pollFn) { 152 | this.pollFns.push(pollFn); 153 | return pollFn; 154 | }, 155 | 156 | url: function(url, replace) { 157 | if (url) { 158 | this.$$url = url; 159 | return this; 160 | } 161 | 162 | return this.$$url; 163 | }, 164 | 165 | cookies: function(name, value) { 166 | if (name) { 167 | if (angular.isUndefined(value)) { 168 | delete this.cookieHash[name]; 169 | } else { 170 | if (angular.isString(value) && //strings only 171 | value.length <= 4096) { //strict cookie storage limits 172 | this.cookieHash[name] = value; 173 | } 174 | } 175 | } else { 176 | if (!angular.equals(this.cookieHash, this.lastCookieHash)) { 177 | this.lastCookieHash = angular.copy(this.cookieHash); 178 | this.cookieHash = angular.copy(this.cookieHash); 179 | } 180 | return this.cookieHash; 181 | } 182 | }, 183 | 184 | notifyWhenNoOutstandingRequests: function(fn) { 185 | fn(); 186 | } 187 | }; 188 | 189 | 190 | /** 191 | * @ngdoc object 192 | * @name ngMock.$exceptionHandlerProvider 193 | * 194 | * @description 195 | * Configures the mock implementation of {@link ng.$exceptionHandler} to rethrow or to log errors 196 | * passed into the `$exceptionHandler`. 197 | */ 198 | 199 | /** 200 | * @ngdoc object 201 | * @name ngMock.$exceptionHandler 202 | * 203 | * @description 204 | * Mock implementation of {@link ng.$exceptionHandler} that rethrows or logs errors passed 205 | * into it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration 206 | * information. 207 | * 208 | * 209 | *
 210 |  *   describe('$exceptionHandlerProvider', function() {
 211 |  *
 212 |  *     it('should capture log messages and exceptions', function() {
 213 |  *
 214 |  *       module(function($exceptionHandlerProvider) {
 215 |  *         $exceptionHandlerProvider.mode('log');
 216 |  *       });
 217 |  *
 218 |  *       inject(function($log, $exceptionHandler, $timeout) {
 219 |  *         $timeout(function() { $log.log(1); });
 220 |  *         $timeout(function() { $log.log(2); throw 'banana peel'; });
 221 |  *         $timeout(function() { $log.log(3); });
 222 |  *         expect($exceptionHandler.errors).toEqual([]);
 223 |  *         expect($log.assertEmpty());
 224 |  *         $timeout.flush();
 225 |  *         expect($exceptionHandler.errors).toEqual(['banana peel']);
 226 |  *         expect($log.log.logs).toEqual([[1], [2], [3]]);
 227 |  *       });
 228 |  *     });
 229 |  *   });
 230 |  * 
231 | */ 232 | 233 | angular.mock.$ExceptionHandlerProvider = function() { 234 | var handler; 235 | 236 | /** 237 | * @ngdoc method 238 | * @name ngMock.$exceptionHandlerProvider#mode 239 | * @methodOf ngMock.$exceptionHandlerProvider 240 | * 241 | * @description 242 | * Sets the logging mode. 243 | * 244 | * @param {string} mode Mode of operation, defaults to `rethrow`. 245 | * 246 | * - `rethrow`: If any errors are passed into the handler in tests, it typically 247 | * means that there is a bug in the application or test, so this mock will 248 | * make these tests fail. 249 | * - `log`: Sometimes it is desirable to test that an error is thrown, for this case the `log` 250 | * mode stores an array of errors in `$exceptionHandler.errors`, to allow later 251 | * assertion of them. See {@link ngMock.$log#assertEmpty assertEmpty()} and 252 | * {@link ngMock.$log#reset reset()} 253 | */ 254 | this.mode = function(mode) { 255 | switch(mode) { 256 | case 'rethrow': 257 | handler = function(e) { 258 | throw e; 259 | }; 260 | break; 261 | case 'log': 262 | var errors = []; 263 | 264 | handler = function(e) { 265 | if (arguments.length == 1) { 266 | errors.push(e); 267 | } else { 268 | errors.push([].slice.call(arguments, 0)); 269 | } 270 | }; 271 | 272 | handler.errors = errors; 273 | break; 274 | default: 275 | throw new Error("Unknown mode '" + mode + "', only 'log'/'rethrow' modes are allowed!"); 276 | } 277 | }; 278 | 279 | this.$get = function() { 280 | return handler; 281 | }; 282 | 283 | this.mode('rethrow'); 284 | }; 285 | 286 | 287 | /** 288 | * @ngdoc service 289 | * @name ngMock.$log 290 | * 291 | * @description 292 | * Mock implementation of {@link ng.$log} that gathers all logged messages in arrays 293 | * (one array per logging level). These arrays are exposed as `logs` property of each of the 294 | * level-specific log function, e.g. for level `error` the array is exposed as `$log.error.logs`. 295 | * 296 | */ 297 | angular.mock.$LogProvider = function() { 298 | var debug = true; 299 | 300 | function concat(array1, array2, index) { 301 | return array1.concat(Array.prototype.slice.call(array2, index)); 302 | } 303 | 304 | this.debugEnabled = function(flag) { 305 | if (angular.isDefined(flag)) { 306 | debug = flag; 307 | return this; 308 | } else { 309 | return debug; 310 | } 311 | }; 312 | 313 | this.$get = function () { 314 | var $log = { 315 | log: function() { $log.log.logs.push(concat([], arguments, 0)); }, 316 | warn: function() { $log.warn.logs.push(concat([], arguments, 0)); }, 317 | info: function() { $log.info.logs.push(concat([], arguments, 0)); }, 318 | error: function() { $log.error.logs.push(concat([], arguments, 0)); }, 319 | debug: function() { 320 | if (debug) { 321 | $log.debug.logs.push(concat([], arguments, 0)); 322 | } 323 | } 324 | }; 325 | 326 | /** 327 | * @ngdoc method 328 | * @name ngMock.$log#reset 329 | * @methodOf ngMock.$log 330 | * 331 | * @description 332 | * Reset all of the logging arrays to empty. 333 | */ 334 | $log.reset = function () { 335 | /** 336 | * @ngdoc property 337 | * @name ngMock.$log#log.logs 338 | * @propertyOf ngMock.$log 339 | * 340 | * @description 341 | * Array of messages logged using {@link ngMock.$log#log}. 342 | * 343 | * @example 344 | *
 345 |        * $log.log('Some Log');
 346 |        * var first = $log.log.logs.unshift();
 347 |        * 
348 | */ 349 | $log.log.logs = []; 350 | /** 351 | * @ngdoc property 352 | * @name ngMock.$log#info.logs 353 | * @propertyOf ngMock.$log 354 | * 355 | * @description 356 | * Array of messages logged using {@link ngMock.$log#info}. 357 | * 358 | * @example 359 | *
 360 |        * $log.info('Some Info');
 361 |        * var first = $log.info.logs.unshift();
 362 |        * 
363 | */ 364 | $log.info.logs = []; 365 | /** 366 | * @ngdoc property 367 | * @name ngMock.$log#warn.logs 368 | * @propertyOf ngMock.$log 369 | * 370 | * @description 371 | * Array of messages logged using {@link ngMock.$log#warn}. 372 | * 373 | * @example 374 | *
 375 |        * $log.warn('Some Warning');
 376 |        * var first = $log.warn.logs.unshift();
 377 |        * 
378 | */ 379 | $log.warn.logs = []; 380 | /** 381 | * @ngdoc property 382 | * @name ngMock.$log#error.logs 383 | * @propertyOf ngMock.$log 384 | * 385 | * @description 386 | * Array of messages logged using {@link ngMock.$log#error}. 387 | * 388 | * @example 389 | *
 390 |        * $log.log('Some Error');
 391 |        * var first = $log.error.logs.unshift();
 392 |        * 
393 | */ 394 | $log.error.logs = []; 395 | /** 396 | * @ngdoc property 397 | * @name ngMock.$log#debug.logs 398 | * @propertyOf ngMock.$log 399 | * 400 | * @description 401 | * Array of messages logged using {@link ngMock.$log#debug}. 402 | * 403 | * @example 404 | *
 405 |        * $log.debug('Some Error');
 406 |        * var first = $log.debug.logs.unshift();
 407 |        * 
408 | */ 409 | $log.debug.logs = []; 410 | }; 411 | 412 | /** 413 | * @ngdoc method 414 | * @name ngMock.$log#assertEmpty 415 | * @methodOf ngMock.$log 416 | * 417 | * @description 418 | * Assert that the all of the logging methods have no logged messages. If messages present, an 419 | * exception is thrown. 420 | */ 421 | $log.assertEmpty = function() { 422 | var errors = []; 423 | angular.forEach(['error', 'warn', 'info', 'log', 'debug'], function(logLevel) { 424 | angular.forEach($log[logLevel].logs, function(log) { 425 | angular.forEach(log, function (logItem) { 426 | errors.push('MOCK $log (' + logLevel + '): ' + String(logItem) + '\n' + 427 | (logItem.stack || '')); 428 | }); 429 | }); 430 | }); 431 | if (errors.length) { 432 | errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or "+ 433 | "an expected log message was not checked and removed:"); 434 | errors.push(''); 435 | throw new Error(errors.join('\n---------\n')); 436 | } 437 | }; 438 | 439 | $log.reset(); 440 | return $log; 441 | }; 442 | }; 443 | 444 | 445 | /** 446 | * @ngdoc service 447 | * @name ngMock.$interval 448 | * 449 | * @description 450 | * Mock implementation of the $interval service. 451 | * 452 | * Use {@link ngMock.$interval#methods_flush `$interval.flush(millis)`} to 453 | * move forward by `millis` milliseconds and trigger any functions scheduled to run in that 454 | * time. 455 | * 456 | * @param {function()} fn A function that should be called repeatedly. 457 | * @param {number} delay Number of milliseconds between each function call. 458 | * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat 459 | * indefinitely. 460 | * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise 461 | * will invoke `fn` within the {@link ng.$rootScope.Scope#methods_$apply $apply} block. 462 | * @returns {promise} A promise which will be notified on each iteration. 463 | */ 464 | angular.mock.$IntervalProvider = function() { 465 | this.$get = ['$rootScope', '$q', 466 | function($rootScope, $q) { 467 | var repeatFns = [], 468 | nextRepeatId = 0, 469 | now = 0; 470 | 471 | var $interval = function(fn, delay, count, invokeApply) { 472 | var deferred = $q.defer(), 473 | promise = deferred.promise, 474 | iteration = 0, 475 | skipApply = (angular.isDefined(invokeApply) && !invokeApply); 476 | 477 | count = (angular.isDefined(count)) ? count : 0, 478 | promise.then(null, null, fn); 479 | 480 | promise.$$intervalId = nextRepeatId; 481 | 482 | function tick() { 483 | deferred.notify(iteration++); 484 | 485 | if (count > 0 && iteration >= count) { 486 | var fnIndex; 487 | deferred.resolve(iteration); 488 | 489 | angular.forEach(repeatFns, function(fn, index) { 490 | if (fn.id === promise.$$intervalId) fnIndex = index; 491 | }); 492 | 493 | if (fnIndex !== undefined) { 494 | repeatFns.splice(fnIndex, 1); 495 | } 496 | } 497 | 498 | if (!skipApply) $rootScope.$apply(); 499 | } 500 | 501 | repeatFns.push({ 502 | nextTime:(now + delay), 503 | delay: delay, 504 | fn: tick, 505 | id: nextRepeatId, 506 | deferred: deferred 507 | }); 508 | repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;}); 509 | 510 | nextRepeatId++; 511 | return promise; 512 | }; 513 | 514 | $interval.cancel = function(promise) { 515 | var fnIndex; 516 | 517 | angular.forEach(repeatFns, function(fn, index) { 518 | if (fn.id === promise.$$intervalId) fnIndex = index; 519 | }); 520 | 521 | if (fnIndex !== undefined) { 522 | repeatFns[fnIndex].deferred.reject('canceled'); 523 | repeatFns.splice(fnIndex, 1); 524 | return true; 525 | } 526 | 527 | return false; 528 | }; 529 | 530 | /** 531 | * @ngdoc method 532 | * @name ngMock.$interval#flush 533 | * @methodOf ngMock.$interval 534 | * @description 535 | * 536 | * Runs interval tasks scheduled to be run in the next `millis` milliseconds. 537 | * 538 | * @param {number=} millis maximum timeout amount to flush up until. 539 | * 540 | * @return {number} The amount of time moved forward. 541 | */ 542 | $interval.flush = function(millis) { 543 | now += millis; 544 | while (repeatFns.length && repeatFns[0].nextTime <= now) { 545 | var task = repeatFns[0]; 546 | task.fn(); 547 | task.nextTime += task.delay; 548 | repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;}); 549 | } 550 | return millis; 551 | }; 552 | 553 | return $interval; 554 | }]; 555 | }; 556 | 557 | 558 | /* jshint -W101 */ 559 | /* The R_ISO8061_STR regex is never going to fit into the 100 char limit! 560 | * This directive should go inside the anonymous function but a bug in JSHint means that it would 561 | * not be enacted early enough to prevent the warning. 562 | */ 563 | (function() { 564 | var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/; 565 | 566 | function jsonStringToDate(string) { 567 | var match; 568 | if (match = string.match(R_ISO8061_STR)) { 569 | var date = new Date(0), 570 | tzHour = 0, 571 | tzMin = 0; 572 | if (match[9]) { 573 | tzHour = int(match[9] + match[10]); 574 | tzMin = int(match[9] + match[11]); 575 | } 576 | date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3])); 577 | date.setUTCHours(int(match[4]||0) - tzHour, 578 | int(match[5]||0) - tzMin, 579 | int(match[6]||0), 580 | int(match[7]||0)); 581 | return date; 582 | } 583 | return string; 584 | } 585 | 586 | function int(str) { 587 | return parseInt(str, 10); 588 | } 589 | 590 | function padNumber(num, digits, trim) { 591 | var neg = ''; 592 | if (num < 0) { 593 | neg = '-'; 594 | num = -num; 595 | } 596 | num = '' + num; 597 | while(num.length < digits) num = '0' + num; 598 | if (trim) 599 | num = num.substr(num.length - digits); 600 | return neg + num; 601 | } 602 | 603 | 604 | /** 605 | * @ngdoc object 606 | * @name angular.mock.TzDate 607 | * @description 608 | * 609 | * *NOTE*: this is not an injectable instance, just a globally available mock class of `Date`. 610 | * 611 | * Mock of the Date type which has its timezone specified via constructor arg. 612 | * 613 | * The main purpose is to create Date-like instances with timezone fixed to the specified timezone 614 | * offset, so that we can test code that depends on local timezone settings without dependency on 615 | * the time zone settings of the machine where the code is running. 616 | * 617 | * @param {number} offset Offset of the *desired* timezone in hours (fractions will be honored) 618 | * @param {(number|string)} timestamp Timestamp representing the desired time in *UTC* 619 | * 620 | * @example 621 | * !!!! WARNING !!!!! 622 | * This is not a complete Date object so only methods that were implemented can be called safely. 623 | * To make matters worse, TzDate instances inherit stuff from Date via a prototype. 624 | * 625 | * We do our best to intercept calls to "unimplemented" methods, but since the list of methods is 626 | * incomplete we might be missing some non-standard methods. This can result in errors like: 627 | * "Date.prototype.foo called on incompatible Object". 628 | * 629 | *
 630 |    * var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z');
 631 |    * newYearInBratislava.getTimezoneOffset() => -60;
 632 |    * newYearInBratislava.getFullYear() => 2010;
 633 |    * newYearInBratislava.getMonth() => 0;
 634 |    * newYearInBratislava.getDate() => 1;
 635 |    * newYearInBratislava.getHours() => 0;
 636 |    * newYearInBratislava.getMinutes() => 0;
 637 |    * newYearInBratislava.getSeconds() => 0;
 638 |    * 
639 | * 640 | */ 641 | angular.mock.TzDate = function (offset, timestamp) { 642 | var self = new Date(0); 643 | if (angular.isString(timestamp)) { 644 | var tsStr = timestamp; 645 | 646 | self.origDate = jsonStringToDate(timestamp); 647 | 648 | timestamp = self.origDate.getTime(); 649 | if (isNaN(timestamp)) 650 | throw { 651 | name: "Illegal Argument", 652 | message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string" 653 | }; 654 | } else { 655 | self.origDate = new Date(timestamp); 656 | } 657 | 658 | var localOffset = new Date(timestamp).getTimezoneOffset(); 659 | self.offsetDiff = localOffset*60*1000 - offset*1000*60*60; 660 | self.date = new Date(timestamp + self.offsetDiff); 661 | 662 | self.getTime = function() { 663 | return self.date.getTime() - self.offsetDiff; 664 | }; 665 | 666 | self.toLocaleDateString = function() { 667 | return self.date.toLocaleDateString(); 668 | }; 669 | 670 | self.getFullYear = function() { 671 | return self.date.getFullYear(); 672 | }; 673 | 674 | self.getMonth = function() { 675 | return self.date.getMonth(); 676 | }; 677 | 678 | self.getDate = function() { 679 | return self.date.getDate(); 680 | }; 681 | 682 | self.getHours = function() { 683 | return self.date.getHours(); 684 | }; 685 | 686 | self.getMinutes = function() { 687 | return self.date.getMinutes(); 688 | }; 689 | 690 | self.getSeconds = function() { 691 | return self.date.getSeconds(); 692 | }; 693 | 694 | self.getMilliseconds = function() { 695 | return self.date.getMilliseconds(); 696 | }; 697 | 698 | self.getTimezoneOffset = function() { 699 | return offset * 60; 700 | }; 701 | 702 | self.getUTCFullYear = function() { 703 | return self.origDate.getUTCFullYear(); 704 | }; 705 | 706 | self.getUTCMonth = function() { 707 | return self.origDate.getUTCMonth(); 708 | }; 709 | 710 | self.getUTCDate = function() { 711 | return self.origDate.getUTCDate(); 712 | }; 713 | 714 | self.getUTCHours = function() { 715 | return self.origDate.getUTCHours(); 716 | }; 717 | 718 | self.getUTCMinutes = function() { 719 | return self.origDate.getUTCMinutes(); 720 | }; 721 | 722 | self.getUTCSeconds = function() { 723 | return self.origDate.getUTCSeconds(); 724 | }; 725 | 726 | self.getUTCMilliseconds = function() { 727 | return self.origDate.getUTCMilliseconds(); 728 | }; 729 | 730 | self.getDay = function() { 731 | return self.date.getDay(); 732 | }; 733 | 734 | // provide this method only on browsers that already have it 735 | if (self.toISOString) { 736 | self.toISOString = function() { 737 | return padNumber(self.origDate.getUTCFullYear(), 4) + '-' + 738 | padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' + 739 | padNumber(self.origDate.getUTCDate(), 2) + 'T' + 740 | padNumber(self.origDate.getUTCHours(), 2) + ':' + 741 | padNumber(self.origDate.getUTCMinutes(), 2) + ':' + 742 | padNumber(self.origDate.getUTCSeconds(), 2) + '.' + 743 | padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z'; 744 | }; 745 | } 746 | 747 | //hide all methods not implemented in this mock that the Date prototype exposes 748 | var unimplementedMethods = ['getUTCDay', 749 | 'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds', 750 | 'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear', 751 | 'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds', 752 | 'setYear', 'toDateString', 'toGMTString', 'toJSON', 'toLocaleFormat', 'toLocaleString', 753 | 'toLocaleTimeString', 'toSource', 'toString', 'toTimeString', 'toUTCString', 'valueOf']; 754 | 755 | angular.forEach(unimplementedMethods, function(methodName) { 756 | self[methodName] = function() { 757 | throw new Error("Method '" + methodName + "' is not implemented in the TzDate mock"); 758 | }; 759 | }); 760 | 761 | return self; 762 | }; 763 | 764 | //make "tzDateInstance instanceof Date" return true 765 | angular.mock.TzDate.prototype = Date.prototype; 766 | })(); 767 | /* jshint +W101 */ 768 | 769 | angular.mock.animate = angular.module('mock.animate', ['ng']) 770 | 771 | .config(['$provide', function($provide) { 772 | 773 | $provide.decorator('$animate', function($delegate) { 774 | var animate = { 775 | queue : [], 776 | enabled : $delegate.enabled, 777 | flushNext : function(name) { 778 | var tick = animate.queue.shift(); 779 | 780 | if (!tick) throw new Error('No animation to be flushed'); 781 | if(tick.method !== name) { 782 | throw new Error('The next animation is not "' + name + 783 | '", but is "' + tick.method + '"'); 784 | } 785 | tick.fn(); 786 | return tick; 787 | } 788 | }; 789 | 790 | angular.forEach(['enter','leave','move','addClass','removeClass'], function(method) { 791 | animate[method] = function() { 792 | var params = arguments; 793 | animate.queue.push({ 794 | method : method, 795 | params : params, 796 | element : angular.isElement(params[0]) && params[0], 797 | parent : angular.isElement(params[1]) && params[1], 798 | after : angular.isElement(params[2]) && params[2], 799 | fn : function() { 800 | $delegate[method].apply($delegate, params); 801 | } 802 | }); 803 | }; 804 | }); 805 | 806 | return animate; 807 | }); 808 | 809 | }]); 810 | 811 | 812 | /** 813 | * @ngdoc function 814 | * @name angular.mock.dump 815 | * @description 816 | * 817 | * *NOTE*: this is not an injectable instance, just a globally available function. 818 | * 819 | * Method for serializing common angular objects (scope, elements, etc..) into strings, useful for 820 | * debugging. 821 | * 822 | * This method is also available on window, where it can be used to display objects on debug 823 | * console. 824 | * 825 | * @param {*} object - any object to turn into string. 826 | * @return {string} a serialized string of the argument 827 | */ 828 | angular.mock.dump = function(object) { 829 | return serialize(object); 830 | 831 | function serialize(object) { 832 | var out; 833 | 834 | if (angular.isElement(object)) { 835 | object = angular.element(object); 836 | out = angular.element('
'); 837 | angular.forEach(object, function(element) { 838 | out.append(angular.element(element).clone()); 839 | }); 840 | out = out.html(); 841 | } else if (angular.isArray(object)) { 842 | out = []; 843 | angular.forEach(object, function(o) { 844 | out.push(serialize(o)); 845 | }); 846 | out = '[ ' + out.join(', ') + ' ]'; 847 | } else if (angular.isObject(object)) { 848 | if (angular.isFunction(object.$eval) && angular.isFunction(object.$apply)) { 849 | out = serializeScope(object); 850 | } else if (object instanceof Error) { 851 | out = object.stack || ('' + object.name + ': ' + object.message); 852 | } else { 853 | // TODO(i): this prevents methods being logged, 854 | // we should have a better way to serialize objects 855 | out = angular.toJson(object, true); 856 | } 857 | } else { 858 | out = String(object); 859 | } 860 | 861 | return out; 862 | } 863 | 864 | function serializeScope(scope, offset) { 865 | offset = offset || ' '; 866 | var log = [offset + 'Scope(' + scope.$id + '): {']; 867 | for ( var key in scope ) { 868 | if (Object.prototype.hasOwnProperty.call(scope, key) && !key.match(/^(\$|this)/)) { 869 | log.push(' ' + key + ': ' + angular.toJson(scope[key])); 870 | } 871 | } 872 | var child = scope.$$childHead; 873 | while(child) { 874 | log.push(serializeScope(child, offset + ' ')); 875 | child = child.$$nextSibling; 876 | } 877 | log.push('}'); 878 | return log.join('\n' + offset); 879 | } 880 | }; 881 | 882 | /** 883 | * @ngdoc object 884 | * @name ngMock.$httpBackend 885 | * @description 886 | * Fake HTTP backend implementation suitable for unit testing applications that use the 887 | * {@link ng.$http $http service}. 888 | * 889 | * *Note*: For fake HTTP backend implementation suitable for end-to-end testing or backend-less 890 | * development please see {@link ngMockE2E.$httpBackend e2e $httpBackend mock}. 891 | * 892 | * During unit testing, we want our unit tests to run quickly and have no external dependencies so 893 | * we don’t want to send {@link https://developer.mozilla.org/en/xmlhttprequest XHR} or 894 | * {@link http://en.wikipedia.org/wiki/JSONP JSONP} requests to a real server. All we really need is 895 | * to verify whether a certain request has been sent or not, or alternatively just let the 896 | * application make requests, respond with pre-trained responses and assert that the end result is 897 | * what we expect it to be. 898 | * 899 | * This mock implementation can be used to respond with static or dynamic responses via the 900 | * `expect` and `when` apis and their shortcuts (`expectGET`, `whenPOST`, etc). 901 | * 902 | * When an Angular application needs some data from a server, it calls the $http service, which 903 | * sends the request to a real server using $httpBackend service. With dependency injection, it is 904 | * easy to inject $httpBackend mock (which has the same API as $httpBackend) and use it to verify 905 | * the requests and respond with some testing data without sending a request to real server. 906 | * 907 | * There are two ways to specify what test data should be returned as http responses by the mock 908 | * backend when the code under test makes http requests: 909 | * 910 | * - `$httpBackend.expect` - specifies a request expectation 911 | * - `$httpBackend.when` - specifies a backend definition 912 | * 913 | * 914 | * # Request Expectations vs Backend Definitions 915 | * 916 | * Request expectations provide a way to make assertions about requests made by the application and 917 | * to define responses for those requests. The test will fail if the expected requests are not made 918 | * or they are made in the wrong order. 919 | * 920 | * Backend definitions allow you to define a fake backend for your application which doesn't assert 921 | * if a particular request was made or not, it just returns a trained response if a request is made. 922 | * The test will pass whether or not the request gets made during testing. 923 | * 924 | * 925 | * 926 | * 927 | * 928 | * 929 | * 930 | * 931 | * 932 | * 933 | * 934 | * 935 | * 936 | * 937 | * 938 | * 939 | * 940 | * 941 | * 942 | * 943 | * 944 | * 945 | * 946 | * 947 | * 948 | * 949 | * 950 | * 951 | * 952 | * 953 | * 954 | * 955 | * 956 | * 957 | *
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
958 | * 959 | * In cases where both backend definitions and request expectations are specified during unit 960 | * testing, the request expectations are evaluated first. 961 | * 962 | * If a request expectation has no response specified, the algorithm will search your backend 963 | * definitions for an appropriate response. 964 | * 965 | * If a request didn't match any expectation or if the expectation doesn't have the response 966 | * defined, the backend definitions are evaluated in sequential order to see if any of them match 967 | * the request. The response from the first matched definition is returned. 968 | * 969 | * 970 | * # Flushing HTTP requests 971 | * 972 | * The $httpBackend used in production, always responds to requests with responses asynchronously. 973 | * If we preserved this behavior in unit testing, we'd have to create async unit tests, which are 974 | * hard to write, follow and maintain. At the same time the testing mock, can't respond 975 | * synchronously because that would change the execution of the code under test. For this reason the 976 | * mock $httpBackend has a `flush()` method, which allows the test to explicitly flush pending 977 | * requests and thus preserving the async api of the backend, while allowing the test to execute 978 | * synchronously. 979 | * 980 | * 981 | * # Unit testing with mock $httpBackend 982 | * The following code shows how to setup and use the mock backend in unit testing a controller. 983 | * First we create the controller under test 984 | * 985 |
 986 |   // The controller code
 987 |   function MyController($scope, $http) {
 988 |     var authToken;
 989 | 
 990 |     $http.get('/auth.py').success(function(data, status, headers) {
 991 |       authToken = headers('A-Token');
 992 |       $scope.user = data;
 993 |     });
 994 | 
 995 |     $scope.saveMessage = function(message) {
 996 |       var headers = { 'Authorization': authToken };
 997 |       $scope.status = 'Saving...';
 998 | 
 999 |       $http.post('/add-msg.py', message, { headers: headers } ).success(function(response) {
1000 |         $scope.status = '';
1001 |       }).error(function() {
1002 |         $scope.status = 'ERROR!';
1003 |       });
1004 |     };
1005 |   }
1006 |   
1007 | * 1008 | * Now we setup the mock backend and create the test specs. 1009 | * 1010 |
1011 |     // testing controller
1012 |     describe('MyController', function() {
1013 |        var $httpBackend, $rootScope, createController;
1014 | 
1015 |        beforeEach(inject(function($injector) {
1016 |          // Set up the mock http service responses
1017 |          $httpBackend = $injector.get('$httpBackend');
1018 |          // backend definition common for all tests
1019 |          $httpBackend.when('GET', '/auth.py').respond({userId: 'userX'}, {'A-Token': 'xxx'});
1020 | 
1021 |          // Get hold of a scope (i.e. the root scope)
1022 |          $rootScope = $injector.get('$rootScope');
1023 |          // The $controller service is used to create instances of controllers
1024 |          var $controller = $injector.get('$controller');
1025 | 
1026 |          createController = function() {
1027 |            return $controller('MyController', {'$scope' : $rootScope });
1028 |          };
1029 |        }));
1030 | 
1031 | 
1032 |        afterEach(function() {
1033 |          $httpBackend.verifyNoOutstandingExpectation();
1034 |          $httpBackend.verifyNoOutstandingRequest();
1035 |        });
1036 | 
1037 | 
1038 |        it('should fetch authentication token', function() {
1039 |          $httpBackend.expectGET('/auth.py');
1040 |          var controller = createController();
1041 |          $httpBackend.flush();
1042 |        });
1043 | 
1044 | 
1045 |        it('should send msg to server', function() {
1046 |          var controller = createController();
1047 |          $httpBackend.flush();
1048 | 
1049 |          // now you don’t care about the authentication, but
1050 |          // the controller will still send the request and
1051 |          // $httpBackend will respond without you having to
1052 |          // specify the expectation and response for this request
1053 | 
1054 |          $httpBackend.expectPOST('/add-msg.py', 'message content').respond(201, '');
1055 |          $rootScope.saveMessage('message content');
1056 |          expect($rootScope.status).toBe('Saving...');
1057 |          $httpBackend.flush();
1058 |          expect($rootScope.status).toBe('');
1059 |        });
1060 | 
1061 | 
1062 |        it('should send auth header', function() {
1063 |          var controller = createController();
1064 |          $httpBackend.flush();
1065 | 
1066 |          $httpBackend.expectPOST('/add-msg.py', undefined, function(headers) {
1067 |            // check if the header was send, if it wasn't the expectation won't
1068 |            // match the request and the test will fail
1069 |            return headers['Authorization'] == 'xxx';
1070 |          }).respond(201, '');
1071 | 
1072 |          $rootScope.saveMessage('whatever');
1073 |          $httpBackend.flush();
1074 |        });
1075 |     });
1076 |    
1077 | */ 1078 | angular.mock.$HttpBackendProvider = function() { 1079 | this.$get = ['$rootScope', createHttpBackendMock]; 1080 | }; 1081 | 1082 | /** 1083 | * General factory function for $httpBackend mock. 1084 | * Returns instance for unit testing (when no arguments specified): 1085 | * - passing through is disabled 1086 | * - auto flushing is disabled 1087 | * 1088 | * Returns instance for e2e testing (when `$delegate` and `$browser` specified): 1089 | * - passing through (delegating request to real backend) is enabled 1090 | * - auto flushing is enabled 1091 | * 1092 | * @param {Object=} $delegate Real $httpBackend instance (allow passing through if specified) 1093 | * @param {Object=} $browser Auto-flushing enabled if specified 1094 | * @return {Object} Instance of $httpBackend mock 1095 | */ 1096 | function createHttpBackendMock($rootScope, $delegate, $browser) { 1097 | var definitions = [], 1098 | expectations = [], 1099 | responses = [], 1100 | responsesPush = angular.bind(responses, responses.push); 1101 | 1102 | function createResponse(status, data, headers) { 1103 | if (angular.isFunction(status)) return status; 1104 | 1105 | return function() { 1106 | return angular.isNumber(status) 1107 | ? [status, data, headers] 1108 | : [200, status, data]; 1109 | }; 1110 | } 1111 | 1112 | // TODO(vojta): change params to: method, url, data, headers, callback 1113 | function $httpBackend(method, url, data, callback, headers, timeout, withCredentials) { 1114 | var xhr = new MockXhr(), 1115 | expectation = expectations[0], 1116 | wasExpected = false; 1117 | 1118 | function prettyPrint(data) { 1119 | return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp) 1120 | ? data 1121 | : angular.toJson(data); 1122 | } 1123 | 1124 | function wrapResponse(wrapped) { 1125 | if (!$browser && timeout && timeout.then) timeout.then(handleTimeout); 1126 | 1127 | return handleResponse; 1128 | 1129 | function handleResponse() { 1130 | var response = wrapped.response(method, url, data, headers); 1131 | xhr.$$respHeaders = response[2]; 1132 | callback(response[0], response[1], xhr.getAllResponseHeaders()); 1133 | } 1134 | 1135 | function handleTimeout() { 1136 | for (var i = 0, ii = responses.length; i < ii; i++) { 1137 | if (responses[i] === handleResponse) { 1138 | responses.splice(i, 1); 1139 | callback(-1, undefined, ''); 1140 | break; 1141 | } 1142 | } 1143 | } 1144 | } 1145 | 1146 | if (expectation && expectation.match(method, url)) { 1147 | if (!expectation.matchData(data)) 1148 | throw new Error('Expected ' + expectation + ' with different data\n' + 1149 | 'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data); 1150 | 1151 | if (!expectation.matchHeaders(headers)) 1152 | throw new Error('Expected ' + expectation + ' with different headers\n' + 1153 | 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' + 1154 | prettyPrint(headers)); 1155 | 1156 | expectations.shift(); 1157 | 1158 | if (expectation.response) { 1159 | responses.push(wrapResponse(expectation)); 1160 | return; 1161 | } 1162 | wasExpected = true; 1163 | } 1164 | 1165 | var i = -1, definition; 1166 | while ((definition = definitions[++i])) { 1167 | if (definition.match(method, url, data, headers || {})) { 1168 | if (definition.response) { 1169 | // if $browser specified, we do auto flush all requests 1170 | ($browser ? $browser.defer : responsesPush)(wrapResponse(definition)); 1171 | } else if (definition.passThrough) { 1172 | $delegate(method, url, data, callback, headers, timeout, withCredentials); 1173 | } else throw new Error('No response defined !'); 1174 | return; 1175 | } 1176 | } 1177 | throw wasExpected ? 1178 | new Error('No response defined !') : 1179 | new Error('Unexpected request: ' + method + ' ' + url + '\n' + 1180 | (expectation ? 'Expected ' + expectation : 'No more request expected')); 1181 | } 1182 | 1183 | /** 1184 | * @ngdoc method 1185 | * @name ngMock.$httpBackend#when 1186 | * @methodOf ngMock.$httpBackend 1187 | * @description 1188 | * Creates a new backend definition. 1189 | * 1190 | * @param {string} method HTTP method. 1191 | * @param {string|RegExp} url HTTP url. 1192 | * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives 1193 | * data string and returns true if the data is as expected. 1194 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header 1195 | * object and returns true if the headers match the current definition. 1196 | * @returns {requestHandler} Returns an object with `respond` method that controls how a matched 1197 | * request is handled. 1198 | * 1199 | * - respond – 1200 | * `{function([status,] data[, headers])|function(function(method, url, data, headers)}` 1201 | * – The respond method takes a set of static data to be returned or a function that can return 1202 | * an array containing response status (number), response data (string) and response headers 1203 | * (Object). 1204 | */ 1205 | $httpBackend.when = function(method, url, data, headers) { 1206 | var definition = new MockHttpExpectation(method, url, data, headers), 1207 | chain = { 1208 | respond: function(status, data, headers) { 1209 | definition.response = createResponse(status, data, headers); 1210 | } 1211 | }; 1212 | 1213 | if ($browser) { 1214 | chain.passThrough = function() { 1215 | definition.passThrough = true; 1216 | }; 1217 | } 1218 | 1219 | definitions.push(definition); 1220 | return chain; 1221 | }; 1222 | 1223 | /** 1224 | * @ngdoc method 1225 | * @name ngMock.$httpBackend#whenGET 1226 | * @methodOf ngMock.$httpBackend 1227 | * @description 1228 | * Creates a new backend definition for GET requests. For more info see `when()`. 1229 | * 1230 | * @param {string|RegExp} url HTTP url. 1231 | * @param {(Object|function(Object))=} headers HTTP headers. 1232 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1233 | * request is handled. 1234 | */ 1235 | 1236 | /** 1237 | * @ngdoc method 1238 | * @name ngMock.$httpBackend#whenHEAD 1239 | * @methodOf ngMock.$httpBackend 1240 | * @description 1241 | * Creates a new backend definition for HEAD requests. For more info see `when()`. 1242 | * 1243 | * @param {string|RegExp} url HTTP url. 1244 | * @param {(Object|function(Object))=} headers HTTP headers. 1245 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1246 | * request is handled. 1247 | */ 1248 | 1249 | /** 1250 | * @ngdoc method 1251 | * @name ngMock.$httpBackend#whenDELETE 1252 | * @methodOf ngMock.$httpBackend 1253 | * @description 1254 | * Creates a new backend definition for DELETE requests. For more info see `when()`. 1255 | * 1256 | * @param {string|RegExp} url HTTP url. 1257 | * @param {(Object|function(Object))=} headers HTTP headers. 1258 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1259 | * request is handled. 1260 | */ 1261 | 1262 | /** 1263 | * @ngdoc method 1264 | * @name ngMock.$httpBackend#whenPOST 1265 | * @methodOf ngMock.$httpBackend 1266 | * @description 1267 | * Creates a new backend definition for POST requests. For more info see `when()`. 1268 | * 1269 | * @param {string|RegExp} url HTTP url. 1270 | * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives 1271 | * data string and returns true if the data is as expected. 1272 | * @param {(Object|function(Object))=} headers HTTP headers. 1273 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1274 | * request is handled. 1275 | */ 1276 | 1277 | /** 1278 | * @ngdoc method 1279 | * @name ngMock.$httpBackend#whenPUT 1280 | * @methodOf ngMock.$httpBackend 1281 | * @description 1282 | * Creates a new backend definition for PUT requests. For more info see `when()`. 1283 | * 1284 | * @param {string|RegExp} url HTTP url. 1285 | * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives 1286 | * data string and returns true if the data is as expected. 1287 | * @param {(Object|function(Object))=} headers HTTP headers. 1288 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1289 | * request is handled. 1290 | */ 1291 | 1292 | /** 1293 | * @ngdoc method 1294 | * @name ngMock.$httpBackend#whenJSONP 1295 | * @methodOf ngMock.$httpBackend 1296 | * @description 1297 | * Creates a new backend definition for JSONP requests. For more info see `when()`. 1298 | * 1299 | * @param {string|RegExp} url HTTP url. 1300 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1301 | * request is handled. 1302 | */ 1303 | createShortMethods('when'); 1304 | 1305 | 1306 | /** 1307 | * @ngdoc method 1308 | * @name ngMock.$httpBackend#expect 1309 | * @methodOf ngMock.$httpBackend 1310 | * @description 1311 | * Creates a new request expectation. 1312 | * 1313 | * @param {string} method HTTP method. 1314 | * @param {string|RegExp} url HTTP url. 1315 | * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that 1316 | * receives data string and returns true if the data is as expected, or Object if request body 1317 | * is in JSON format. 1318 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header 1319 | * object and returns true if the headers match the current expectation. 1320 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1321 | * request is handled. 1322 | * 1323 | * - respond – 1324 | * `{function([status,] data[, headers])|function(function(method, url, data, headers)}` 1325 | * – The respond method takes a set of static data to be returned or a function that can return 1326 | * an array containing response status (number), response data (string) and response headers 1327 | * (Object). 1328 | */ 1329 | $httpBackend.expect = function(method, url, data, headers) { 1330 | var expectation = new MockHttpExpectation(method, url, data, headers); 1331 | expectations.push(expectation); 1332 | return { 1333 | respond: function(status, data, headers) { 1334 | expectation.response = createResponse(status, data, headers); 1335 | } 1336 | }; 1337 | }; 1338 | 1339 | 1340 | /** 1341 | * @ngdoc method 1342 | * @name ngMock.$httpBackend#expectGET 1343 | * @methodOf ngMock.$httpBackend 1344 | * @description 1345 | * Creates a new request expectation for GET requests. For more info see `expect()`. 1346 | * 1347 | * @param {string|RegExp} url HTTP url. 1348 | * @param {Object=} headers HTTP headers. 1349 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1350 | * request is handled. See #expect for more info. 1351 | */ 1352 | 1353 | /** 1354 | * @ngdoc method 1355 | * @name ngMock.$httpBackend#expectHEAD 1356 | * @methodOf ngMock.$httpBackend 1357 | * @description 1358 | * Creates a new request expectation for HEAD requests. For more info see `expect()`. 1359 | * 1360 | * @param {string|RegExp} url HTTP url. 1361 | * @param {Object=} headers HTTP headers. 1362 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1363 | * request is handled. 1364 | */ 1365 | 1366 | /** 1367 | * @ngdoc method 1368 | * @name ngMock.$httpBackend#expectDELETE 1369 | * @methodOf ngMock.$httpBackend 1370 | * @description 1371 | * Creates a new request expectation for DELETE requests. For more info see `expect()`. 1372 | * 1373 | * @param {string|RegExp} url HTTP url. 1374 | * @param {Object=} headers HTTP headers. 1375 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1376 | * request is handled. 1377 | */ 1378 | 1379 | /** 1380 | * @ngdoc method 1381 | * @name ngMock.$httpBackend#expectPOST 1382 | * @methodOf ngMock.$httpBackend 1383 | * @description 1384 | * Creates a new request expectation for POST requests. For more info see `expect()`. 1385 | * 1386 | * @param {string|RegExp} url HTTP url. 1387 | * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that 1388 | * receives data string and returns true if the data is as expected, or Object if request body 1389 | * is in JSON format. 1390 | * @param {Object=} headers HTTP headers. 1391 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1392 | * request is handled. 1393 | */ 1394 | 1395 | /** 1396 | * @ngdoc method 1397 | * @name ngMock.$httpBackend#expectPUT 1398 | * @methodOf ngMock.$httpBackend 1399 | * @description 1400 | * Creates a new request expectation for PUT requests. For more info see `expect()`. 1401 | * 1402 | * @param {string|RegExp} url HTTP url. 1403 | * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that 1404 | * receives data string and returns true if the data is as expected, or Object if request body 1405 | * is in JSON format. 1406 | * @param {Object=} headers HTTP headers. 1407 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1408 | * request is handled. 1409 | */ 1410 | 1411 | /** 1412 | * @ngdoc method 1413 | * @name ngMock.$httpBackend#expectPATCH 1414 | * @methodOf ngMock.$httpBackend 1415 | * @description 1416 | * Creates a new request expectation for PATCH requests. For more info see `expect()`. 1417 | * 1418 | * @param {string|RegExp} url HTTP url. 1419 | * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that 1420 | * receives data string and returns true if the data is as expected, or Object if request body 1421 | * is in JSON format. 1422 | * @param {Object=} headers HTTP headers. 1423 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1424 | * request is handled. 1425 | */ 1426 | 1427 | /** 1428 | * @ngdoc method 1429 | * @name ngMock.$httpBackend#expectJSONP 1430 | * @methodOf ngMock.$httpBackend 1431 | * @description 1432 | * Creates a new request expectation for JSONP requests. For more info see `expect()`. 1433 | * 1434 | * @param {string|RegExp} url HTTP url. 1435 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1436 | * request is handled. 1437 | */ 1438 | createShortMethods('expect'); 1439 | 1440 | 1441 | /** 1442 | * @ngdoc method 1443 | * @name ngMock.$httpBackend#flush 1444 | * @methodOf ngMock.$httpBackend 1445 | * @description 1446 | * Flushes all pending requests using the trained responses. 1447 | * 1448 | * @param {number=} count Number of responses to flush (in the order they arrived). If undefined, 1449 | * all pending requests will be flushed. If there are no pending requests when the flush method 1450 | * is called an exception is thrown (as this typically a sign of programming error). 1451 | */ 1452 | $httpBackend.flush = function(count) { 1453 | $rootScope.$digest(); 1454 | if (!responses.length) throw new Error('No pending request to flush !'); 1455 | 1456 | if (angular.isDefined(count)) { 1457 | while (count--) { 1458 | if (!responses.length) throw new Error('No more pending request to flush !'); 1459 | responses.shift()(); 1460 | } 1461 | } else { 1462 | while (responses.length) { 1463 | responses.shift()(); 1464 | } 1465 | } 1466 | $httpBackend.verifyNoOutstandingExpectation(); 1467 | }; 1468 | 1469 | 1470 | /** 1471 | * @ngdoc method 1472 | * @name ngMock.$httpBackend#verifyNoOutstandingExpectation 1473 | * @methodOf ngMock.$httpBackend 1474 | * @description 1475 | * Verifies that all of the requests defined via the `expect` api were made. If any of the 1476 | * requests were not made, verifyNoOutstandingExpectation throws an exception. 1477 | * 1478 | * Typically, you would call this method following each test case that asserts requests using an 1479 | * "afterEach" clause. 1480 | * 1481 | *
1482 |    *   afterEach($httpBackend.verifyNoOutstandingExpectation);
1483 |    * 
1484 | */ 1485 | $httpBackend.verifyNoOutstandingExpectation = function() { 1486 | $rootScope.$digest(); 1487 | if (expectations.length) { 1488 | throw new Error('Unsatisfied requests: ' + expectations.join(', ')); 1489 | } 1490 | }; 1491 | 1492 | 1493 | /** 1494 | * @ngdoc method 1495 | * @name ngMock.$httpBackend#verifyNoOutstandingRequest 1496 | * @methodOf ngMock.$httpBackend 1497 | * @description 1498 | * Verifies that there are no outstanding requests that need to be flushed. 1499 | * 1500 | * Typically, you would call this method following each test case that asserts requests using an 1501 | * "afterEach" clause. 1502 | * 1503 | *
1504 |    *   afterEach($httpBackend.verifyNoOutstandingRequest);
1505 |    * 
1506 | */ 1507 | $httpBackend.verifyNoOutstandingRequest = function() { 1508 | if (responses.length) { 1509 | throw new Error('Unflushed requests: ' + responses.length); 1510 | } 1511 | }; 1512 | 1513 | 1514 | /** 1515 | * @ngdoc method 1516 | * @name ngMock.$httpBackend#resetExpectations 1517 | * @methodOf ngMock.$httpBackend 1518 | * @description 1519 | * Resets all request expectations, but preserves all backend definitions. Typically, you would 1520 | * call resetExpectations during a multiple-phase test when you want to reuse the same instance of 1521 | * $httpBackend mock. 1522 | */ 1523 | $httpBackend.resetExpectations = function() { 1524 | expectations.length = 0; 1525 | responses.length = 0; 1526 | }; 1527 | 1528 | return $httpBackend; 1529 | 1530 | 1531 | function createShortMethods(prefix) { 1532 | angular.forEach(['GET', 'DELETE', 'JSONP'], function(method) { 1533 | $httpBackend[prefix + method] = function(url, headers) { 1534 | return $httpBackend[prefix](method, url, undefined, headers); 1535 | }; 1536 | }); 1537 | 1538 | angular.forEach(['PUT', 'POST', 'PATCH'], function(method) { 1539 | $httpBackend[prefix + method] = function(url, data, headers) { 1540 | return $httpBackend[prefix](method, url, data, headers); 1541 | }; 1542 | }); 1543 | } 1544 | } 1545 | 1546 | function MockHttpExpectation(method, url, data, headers) { 1547 | 1548 | this.data = data; 1549 | this.headers = headers; 1550 | 1551 | this.match = function(m, u, d, h) { 1552 | if (method != m) return false; 1553 | if (!this.matchUrl(u)) return false; 1554 | if (angular.isDefined(d) && !this.matchData(d)) return false; 1555 | if (angular.isDefined(h) && !this.matchHeaders(h)) return false; 1556 | return true; 1557 | }; 1558 | 1559 | this.matchUrl = function(u) { 1560 | if (!url) return true; 1561 | if (angular.isFunction(url.test)) return url.test(u); 1562 | return url == u; 1563 | }; 1564 | 1565 | this.matchHeaders = function(h) { 1566 | if (angular.isUndefined(headers)) return true; 1567 | if (angular.isFunction(headers)) return headers(h); 1568 | return angular.equals(headers, h); 1569 | }; 1570 | 1571 | this.matchData = function(d) { 1572 | if (angular.isUndefined(data)) return true; 1573 | if (data && angular.isFunction(data.test)) return data.test(d); 1574 | if (data && angular.isFunction(data)) return data(d); 1575 | if (data && !angular.isString(data)) return angular.toJson(data) == d; 1576 | return data == d; 1577 | }; 1578 | 1579 | this.toString = function() { 1580 | return method + ' ' + url; 1581 | }; 1582 | } 1583 | 1584 | function MockXhr() { 1585 | 1586 | // hack for testing $http, $httpBackend 1587 | MockXhr.$$lastInstance = this; 1588 | 1589 | this.open = function(method, url, async) { 1590 | this.$$method = method; 1591 | this.$$url = url; 1592 | this.$$async = async; 1593 | this.$$reqHeaders = {}; 1594 | this.$$respHeaders = {}; 1595 | }; 1596 | 1597 | this.send = function(data) { 1598 | this.$$data = data; 1599 | }; 1600 | 1601 | this.setRequestHeader = function(key, value) { 1602 | this.$$reqHeaders[key] = value; 1603 | }; 1604 | 1605 | this.getResponseHeader = function(name) { 1606 | // the lookup must be case insensitive, 1607 | // that's why we try two quick lookups first and full scan last 1608 | var header = this.$$respHeaders[name]; 1609 | if (header) return header; 1610 | 1611 | name = angular.lowercase(name); 1612 | header = this.$$respHeaders[name]; 1613 | if (header) return header; 1614 | 1615 | header = undefined; 1616 | angular.forEach(this.$$respHeaders, function(headerVal, headerName) { 1617 | if (!header && angular.lowercase(headerName) == name) header = headerVal; 1618 | }); 1619 | return header; 1620 | }; 1621 | 1622 | this.getAllResponseHeaders = function() { 1623 | var lines = []; 1624 | 1625 | angular.forEach(this.$$respHeaders, function(value, key) { 1626 | lines.push(key + ': ' + value); 1627 | }); 1628 | return lines.join('\n'); 1629 | }; 1630 | 1631 | this.abort = angular.noop; 1632 | } 1633 | 1634 | 1635 | /** 1636 | * @ngdoc function 1637 | * @name ngMock.$timeout 1638 | * @description 1639 | * 1640 | * This service is just a simple decorator for {@link ng.$timeout $timeout} service 1641 | * that adds a "flush" and "verifyNoPendingTasks" methods. 1642 | */ 1643 | 1644 | angular.mock.$TimeoutDecorator = function($delegate, $browser) { 1645 | 1646 | /** 1647 | * @ngdoc method 1648 | * @name ngMock.$timeout#flush 1649 | * @methodOf ngMock.$timeout 1650 | * @description 1651 | * 1652 | * Flushes the queue of pending tasks. 1653 | * 1654 | * @param {number=} delay maximum timeout amount to flush up until 1655 | */ 1656 | $delegate.flush = function(delay) { 1657 | $browser.defer.flush(delay); 1658 | }; 1659 | 1660 | /** 1661 | * @ngdoc method 1662 | * @name ngMock.$timeout#flushNext 1663 | * @methodOf ngMock.$timeout 1664 | * @description 1665 | * 1666 | * Flushes the next timeout in the queue and compares it to the provided delay 1667 | * 1668 | * @param {number=} expectedDelay the delay value that will be asserted against the delay of the 1669 | * next timeout function 1670 | */ 1671 | $delegate.flushNext = function(expectedDelay) { 1672 | $browser.defer.flushNext(expectedDelay); 1673 | }; 1674 | 1675 | /** 1676 | * @ngdoc method 1677 | * @name ngMock.$timeout#verifyNoPendingTasks 1678 | * @methodOf ngMock.$timeout 1679 | * @description 1680 | * 1681 | * Verifies that there are no pending tasks that need to be flushed. 1682 | */ 1683 | $delegate.verifyNoPendingTasks = function() { 1684 | if ($browser.deferredFns.length) { 1685 | throw new Error('Deferred tasks to flush (' + $browser.deferredFns.length + '): ' + 1686 | formatPendingTasksAsString($browser.deferredFns)); 1687 | } 1688 | }; 1689 | 1690 | function formatPendingTasksAsString(tasks) { 1691 | var result = []; 1692 | angular.forEach(tasks, function(task) { 1693 | result.push('{id: ' + task.id + ', ' + 'time: ' + task.time + '}'); 1694 | }); 1695 | 1696 | return result.join(', '); 1697 | } 1698 | 1699 | return $delegate; 1700 | }; 1701 | 1702 | /** 1703 | * 1704 | */ 1705 | angular.mock.$RootElementProvider = function() { 1706 | this.$get = function() { 1707 | return angular.element('
'); 1708 | }; 1709 | }; 1710 | 1711 | /** 1712 | * @ngdoc overview 1713 | * @name ngMock 1714 | * @description 1715 | * 1716 | * # ngMock 1717 | * 1718 | * The `ngMock` module providers support to inject and mock Angular services into unit tests. 1719 | * In addition, ngMock also extends various core ng services such that they can be 1720 | * inspected and controlled in a synchronous manner within test code. 1721 | * 1722 | * {@installModule mocks} 1723 | * 1724 | *
1725 | * 1726 | */ 1727 | angular.module('ngMock', ['ng']).provider({ 1728 | $browser: angular.mock.$BrowserProvider, 1729 | $exceptionHandler: angular.mock.$ExceptionHandlerProvider, 1730 | $log: angular.mock.$LogProvider, 1731 | $interval: angular.mock.$IntervalProvider, 1732 | $httpBackend: angular.mock.$HttpBackendProvider, 1733 | $rootElement: angular.mock.$RootElementProvider 1734 | }).config(['$provide', function($provide) { 1735 | $provide.decorator('$timeout', angular.mock.$TimeoutDecorator); 1736 | }]); 1737 | 1738 | /** 1739 | * @ngdoc overview 1740 | * @name ngMockE2E 1741 | * @description 1742 | * 1743 | * The `ngMockE2E` is an angular module which contains mocks suitable for end-to-end testing. 1744 | * Currently there is only one mock present in this module - 1745 | * the {@link ngMockE2E.$httpBackend e2e $httpBackend} mock. 1746 | */ 1747 | angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { 1748 | $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator); 1749 | }]); 1750 | 1751 | /** 1752 | * @ngdoc object 1753 | * @name ngMockE2E.$httpBackend 1754 | * @description 1755 | * Fake HTTP backend implementation suitable for end-to-end testing or backend-less development of 1756 | * applications that use the {@link ng.$http $http service}. 1757 | * 1758 | * *Note*: For fake http backend implementation suitable for unit testing please see 1759 | * {@link ngMock.$httpBackend unit-testing $httpBackend mock}. 1760 | * 1761 | * This implementation can be used to respond with static or dynamic responses via the `when` api 1762 | * and its shortcuts (`whenGET`, `whenPOST`, etc) and optionally pass through requests to the 1763 | * real $httpBackend for specific requests (e.g. to interact with certain remote apis or to fetch 1764 | * templates from a webserver). 1765 | * 1766 | * As opposed to unit-testing, in an end-to-end testing scenario or in scenario when an application 1767 | * is being developed with the real backend api replaced with a mock, it is often desirable for 1768 | * certain category of requests to bypass the mock and issue a real http request (e.g. to fetch 1769 | * templates or static files from the webserver). To configure the backend with this behavior 1770 | * use the `passThrough` request handler of `when` instead of `respond`. 1771 | * 1772 | * Additionally, we don't want to manually have to flush mocked out requests like we do during unit 1773 | * testing. For this reason the e2e $httpBackend automatically flushes mocked out requests 1774 | * automatically, closely simulating the behavior of the XMLHttpRequest object. 1775 | * 1776 | * To setup the application to run with this http backend, you have to create a module that depends 1777 | * on the `ngMockE2E` and your application modules and defines the fake backend: 1778 | * 1779 | *
1780 |  *   myAppDev = angular.module('myAppDev', ['myApp', 'ngMockE2E']);
1781 |  *   myAppDev.run(function($httpBackend) {
1782 |  *     phones = [{name: 'phone1'}, {name: 'phone2'}];
1783 |  *
1784 |  *     // returns the current list of phones
1785 |  *     $httpBackend.whenGET('/phones').respond(phones);
1786 |  *
1787 |  *     // adds a new phone to the phones array
1788 |  *     $httpBackend.whenPOST('/phones').respond(function(method, url, data) {
1789 |  *       phones.push(angular.fromJson(data));
1790 |  *     });
1791 |  *     $httpBackend.whenGET(/^\/templates\//).passThrough();
1792 |  *     //...
1793 |  *   });
1794 |  * 
1795 | * 1796 | * Afterwards, bootstrap your app with this new module. 1797 | */ 1798 | 1799 | /** 1800 | * @ngdoc method 1801 | * @name ngMockE2E.$httpBackend#when 1802 | * @methodOf ngMockE2E.$httpBackend 1803 | * @description 1804 | * Creates a new backend definition. 1805 | * 1806 | * @param {string} method HTTP method. 1807 | * @param {string|RegExp} url HTTP url. 1808 | * @param {(string|RegExp)=} data HTTP request body. 1809 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header 1810 | * object and returns true if the headers match the current definition. 1811 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1812 | * control how a matched request is handled. 1813 | * 1814 | * - respond – 1815 | * `{function([status,] data[, headers])|function(function(method, url, data, headers)}` 1816 | * – The respond method takes a set of static data to be returned or a function that can return 1817 | * an array containing response status (number), response data (string) and response headers 1818 | * (Object). 1819 | * - passThrough – `{function()}` – Any request matching a backend definition with `passThrough` 1820 | * handler, will be pass through to the real backend (an XHR request will be made to the 1821 | * server. 1822 | */ 1823 | 1824 | /** 1825 | * @ngdoc method 1826 | * @name ngMockE2E.$httpBackend#whenGET 1827 | * @methodOf ngMockE2E.$httpBackend 1828 | * @description 1829 | * Creates a new backend definition for GET requests. For more info see `when()`. 1830 | * 1831 | * @param {string|RegExp} url HTTP url. 1832 | * @param {(Object|function(Object))=} headers HTTP headers. 1833 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1834 | * control how a matched request is handled. 1835 | */ 1836 | 1837 | /** 1838 | * @ngdoc method 1839 | * @name ngMockE2E.$httpBackend#whenHEAD 1840 | * @methodOf ngMockE2E.$httpBackend 1841 | * @description 1842 | * Creates a new backend definition for HEAD requests. For more info see `when()`. 1843 | * 1844 | * @param {string|RegExp} url HTTP url. 1845 | * @param {(Object|function(Object))=} headers HTTP headers. 1846 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1847 | * control how a matched request is handled. 1848 | */ 1849 | 1850 | /** 1851 | * @ngdoc method 1852 | * @name ngMockE2E.$httpBackend#whenDELETE 1853 | * @methodOf ngMockE2E.$httpBackend 1854 | * @description 1855 | * Creates a new backend definition for DELETE requests. For more info see `when()`. 1856 | * 1857 | * @param {string|RegExp} url HTTP url. 1858 | * @param {(Object|function(Object))=} headers HTTP headers. 1859 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1860 | * control how a matched request is handled. 1861 | */ 1862 | 1863 | /** 1864 | * @ngdoc method 1865 | * @name ngMockE2E.$httpBackend#whenPOST 1866 | * @methodOf ngMockE2E.$httpBackend 1867 | * @description 1868 | * Creates a new backend definition for POST requests. For more info see `when()`. 1869 | * 1870 | * @param {string|RegExp} url HTTP url. 1871 | * @param {(string|RegExp)=} data HTTP request body. 1872 | * @param {(Object|function(Object))=} headers HTTP headers. 1873 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1874 | * control how a matched request is handled. 1875 | */ 1876 | 1877 | /** 1878 | * @ngdoc method 1879 | * @name ngMockE2E.$httpBackend#whenPUT 1880 | * @methodOf ngMockE2E.$httpBackend 1881 | * @description 1882 | * Creates a new backend definition for PUT requests. For more info see `when()`. 1883 | * 1884 | * @param {string|RegExp} url HTTP url. 1885 | * @param {(string|RegExp)=} data HTTP request body. 1886 | * @param {(Object|function(Object))=} headers HTTP headers. 1887 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1888 | * control how a matched request is handled. 1889 | */ 1890 | 1891 | /** 1892 | * @ngdoc method 1893 | * @name ngMockE2E.$httpBackend#whenPATCH 1894 | * @methodOf ngMockE2E.$httpBackend 1895 | * @description 1896 | * Creates a new backend definition for PATCH requests. For more info see `when()`. 1897 | * 1898 | * @param {string|RegExp} url HTTP url. 1899 | * @param {(string|RegExp)=} data HTTP request body. 1900 | * @param {(Object|function(Object))=} headers HTTP headers. 1901 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1902 | * control how a matched request is handled. 1903 | */ 1904 | 1905 | /** 1906 | * @ngdoc method 1907 | * @name ngMockE2E.$httpBackend#whenJSONP 1908 | * @methodOf ngMockE2E.$httpBackend 1909 | * @description 1910 | * Creates a new backend definition for JSONP requests. For more info see `when()`. 1911 | * 1912 | * @param {string|RegExp} url HTTP url. 1913 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1914 | * control how a matched request is handled. 1915 | */ 1916 | angular.mock.e2e = {}; 1917 | angular.mock.e2e.$httpBackendDecorator = 1918 | ['$rootScope', '$delegate', '$browser', createHttpBackendMock]; 1919 | 1920 | 1921 | angular.mock.clearDataCache = function() { 1922 | var key, 1923 | cache = angular.element.cache; 1924 | 1925 | for(key in cache) { 1926 | if (Object.prototype.hasOwnProperty.call(cache,key)) { 1927 | var handle = cache[key].handle; 1928 | 1929 | handle && angular.element(handle.elem).off(); 1930 | delete cache[key]; 1931 | } 1932 | } 1933 | }; 1934 | 1935 | 1936 | 1937 | (window.jasmine || window.mocha) && (function(window) { 1938 | 1939 | var currentSpec = null; 1940 | 1941 | beforeEach(function() { 1942 | currentSpec = this; 1943 | }); 1944 | 1945 | afterEach(function() { 1946 | var injector = currentSpec.$injector; 1947 | 1948 | currentSpec.$injector = null; 1949 | currentSpec.$modules = null; 1950 | currentSpec = null; 1951 | 1952 | if (injector) { 1953 | injector.get('$rootElement').off(); 1954 | injector.get('$browser').pollFns.length = 0; 1955 | } 1956 | 1957 | angular.mock.clearDataCache(); 1958 | 1959 | // clean up jquery's fragment cache 1960 | angular.forEach(angular.element.fragments, function(val, key) { 1961 | delete angular.element.fragments[key]; 1962 | }); 1963 | 1964 | MockXhr.$$lastInstance = null; 1965 | 1966 | angular.forEach(angular.callbacks, function(val, key) { 1967 | delete angular.callbacks[key]; 1968 | }); 1969 | angular.callbacks.counter = 0; 1970 | }); 1971 | 1972 | function isSpecRunning() { 1973 | return currentSpec && (window.mocha || currentSpec.queue.running); 1974 | } 1975 | 1976 | /** 1977 | * @ngdoc function 1978 | * @name angular.mock.module 1979 | * @description 1980 | * 1981 | * *NOTE*: This function is also published on window for easy access.
1982 | * 1983 | * This function registers a module configuration code. It collects the configuration information 1984 | * which will be used when the injector is created by {@link angular.mock.inject inject}. 1985 | * 1986 | * See {@link angular.mock.inject inject} for usage example 1987 | * 1988 | * @param {...(string|Function|Object)} fns any number of modules which are represented as string 1989 | * aliases or as anonymous module initialization functions. The modules are used to 1990 | * configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. If an 1991 | * object literal is passed they will be register as values in the module, the key being 1992 | * the module name and the value being what is returned. 1993 | */ 1994 | window.module = angular.mock.module = function() { 1995 | var moduleFns = Array.prototype.slice.call(arguments, 0); 1996 | return isSpecRunning() ? workFn() : workFn; 1997 | ///////////////////// 1998 | function workFn() { 1999 | if (currentSpec.$injector) { 2000 | throw new Error('Injector already created, can not register a module!'); 2001 | } else { 2002 | var modules = currentSpec.$modules || (currentSpec.$modules = []); 2003 | angular.forEach(moduleFns, function(module) { 2004 | if (angular.isObject(module) && !angular.isArray(module)) { 2005 | modules.push(function($provide) { 2006 | angular.forEach(module, function(value, key) { 2007 | $provide.value(key, value); 2008 | }); 2009 | }); 2010 | } else { 2011 | modules.push(module); 2012 | } 2013 | }); 2014 | } 2015 | } 2016 | }; 2017 | 2018 | /** 2019 | * @ngdoc function 2020 | * @name angular.mock.inject 2021 | * @description 2022 | * 2023 | * *NOTE*: This function is also published on window for easy access.
2024 | * 2025 | * The inject function wraps a function into an injectable function. The inject() creates new 2026 | * instance of {@link AUTO.$injector $injector} per test, which is then used for 2027 | * resolving references. 2028 | * 2029 | * 2030 | * ## Resolving References (Underscore Wrapping) 2031 | * Often, we would like to inject a reference once, in a `beforeEach()` block and reuse this 2032 | * in multiple `it()` clauses. To be able to do this we must assign the reference to a variable 2033 | * that is declared in the scope of the `describe()` block. Since we would, most likely, want 2034 | * the variable to have the same name of the reference we have a problem, since the parameter 2035 | * to the `inject()` function would hide the outer variable. 2036 | * 2037 | * To help with this, the injected parameters can, optionally, be enclosed with underscores. 2038 | * These are ignored by the injector when the reference name is resolved. 2039 | * 2040 | * For example, the parameter `_myService_` would be resolved as the reference `myService`. 2041 | * Since it is available in the function body as _myService_, we can then assign it to a variable 2042 | * defined in an outer scope. 2043 | * 2044 | * ``` 2045 | * // Defined out reference variable outside 2046 | * var myService; 2047 | * 2048 | * // Wrap the parameter in underscores 2049 | * beforeEach( inject( function(_myService_){ 2050 | * myService = _myService_; 2051 | * })); 2052 | * 2053 | * // Use myService in a series of tests. 2054 | * it('makes use of myService', function() { 2055 | * myService.doStuff(); 2056 | * }); 2057 | * 2058 | * ``` 2059 | * 2060 | * See also {@link angular.mock.module angular.mock.module} 2061 | * 2062 | * ## Example 2063 | * Example of what a typical jasmine tests looks like with the inject method. 2064 | *
2065 |    *
2066 |    *   angular.module('myApplicationModule', [])
2067 |    *       .value('mode', 'app')
2068 |    *       .value('version', 'v1.0.1');
2069 |    *
2070 |    *
2071 |    *   describe('MyApp', function() {
2072 |    *
2073 |    *     // You need to load modules that you want to test,
2074 |    *     // it loads only the "ng" module by default.
2075 |    *     beforeEach(module('myApplicationModule'));
2076 |    *
2077 |    *
2078 |    *     // inject() is used to inject arguments of all given functions
2079 |    *     it('should provide a version', inject(function(mode, version) {
2080 |    *       expect(version).toEqual('v1.0.1');
2081 |    *       expect(mode).toEqual('app');
2082 |    *     }));
2083 |    *
2084 |    *
2085 |    *     // The inject and module method can also be used inside of the it or beforeEach
2086 |    *     it('should override a version and test the new version is injected', function() {
2087 |    *       // module() takes functions or strings (module aliases)
2088 |    *       module(function($provide) {
2089 |    *         $provide.value('version', 'overridden'); // override version here
2090 |    *       });
2091 |    *
2092 |    *       inject(function(version) {
2093 |    *         expect(version).toEqual('overridden');
2094 |    *       });
2095 |    *     });
2096 |    *   });
2097 |    *
2098 |    * 
2099 | * 2100 | * @param {...Function} fns any number of functions which will be injected using the injector. 2101 | */ 2102 | window.inject = angular.mock.inject = function() { 2103 | var blockFns = Array.prototype.slice.call(arguments, 0); 2104 | var errorForStack = new Error('Declaration Location'); 2105 | return isSpecRunning() ? workFn() : workFn; 2106 | ///////////////////// 2107 | function workFn() { 2108 | var modules = currentSpec.$modules || []; 2109 | 2110 | modules.unshift('ngMock'); 2111 | modules.unshift('ng'); 2112 | var injector = currentSpec.$injector; 2113 | if (!injector) { 2114 | injector = currentSpec.$injector = angular.injector(modules); 2115 | } 2116 | for(var i = 0, ii = blockFns.length; i < ii; i++) { 2117 | try { 2118 | /* jshint -W040 *//* Jasmine explicitly provides a `this` object when calling functions */ 2119 | injector.invoke(blockFns[i] || angular.noop, this); 2120 | /* jshint +W040 */ 2121 | } catch (e) { 2122 | if(e.stack && errorForStack) e.stack += '\n' + errorForStack.stack; 2123 | throw e; 2124 | } finally { 2125 | errorForStack = null; 2126 | } 2127 | } 2128 | } 2129 | }; 2130 | })(window); -------------------------------------------------------------------------------- /test/ng-socket-io.spec.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | describe('socket-io module', function() { 5 | var $rootScope, io, socket, currentCallback, sut; 6 | 7 | beforeEach(function() { 8 | module('socket-io'); 9 | 10 | socket = jasmine.createSpyObj('socket', ['on', 'once', 'removeListener', 'removeAllListeners', 'emit']); 11 | socket.on.andCallFake(function(name, callback) { currentCallback = callback; }); 12 | socket.once = socket.on; 13 | socket.emit.andCallFake(function(name, data, callback) { currentCallback = callback; }); 14 | 15 | io = jasmine.createSpyObj('io', ['connect']); 16 | io.connect.andReturn(socket); 17 | 18 | module(function($provide) { 19 | $provide.value('io', io); 20 | }); 21 | 22 | inject(function(_$rootScope_, _socket_) { 23 | $rootScope = _$rootScope_; 24 | sut = _socket_; 25 | }); 26 | 27 | spyOn($rootScope, '$digest'); 28 | }); 29 | 30 | describe('on method', function() { 31 | it('adds a listener to an event', function() { 32 | // Arrange 33 | var callback = jasmine.createSpy(); 34 | 35 | // Act 36 | sut.on('event', callback); 37 | 38 | // Assert 39 | expect(socket.on).toHaveBeenCalledWith('event', jasmine.any(Function)); 40 | }); 41 | 42 | it('calls the callback and triggers a digest cycle', function() { 43 | // Arrange 44 | var callback = jasmine.createSpy(); 45 | sut.on('foo', callback); 46 | 47 | // Act 48 | currentCallback('some data'); 49 | 50 | // Assert 51 | expect(callback).toHaveBeenCalledWith('some data'); 52 | expect($rootScope.$digest).toHaveBeenCalled(); 53 | }); 54 | 55 | it('binds a listener to the scope so it gets removed when the scope is destroyed', function() { 56 | // Arrange 57 | var callback = jasmine.createSpy(); 58 | 59 | // Act 60 | sut.on('foo', callback).bindTo($rootScope); 61 | $rootScope.$broadcast('$destroy'); 62 | $rootScope.$digest(); 63 | 64 | // Assert 65 | expect(socket.removeListener).toHaveBeenCalledWith('foo', currentCallback); 66 | }); 67 | }); 68 | 69 | describe('once method', function() { 70 | it('adds a listener to an event that will trigger only once', function() { 71 | // Arrange 72 | var callback = jasmine.createSpy(); 73 | 74 | // Act 75 | sut.once('event', callback); 76 | 77 | // Assert 78 | expect(socket.once).toHaveBeenCalledWith('event', jasmine.any(Function)); 79 | }); 80 | 81 | it('calls the callback and triggers a digest cycle', function() { 82 | // Arrange 83 | var callback = jasmine.createSpy(); 84 | sut.once('foo', callback); 85 | 86 | // Act 87 | currentCallback('some data'); 88 | 89 | // Assert 90 | expect(callback).toHaveBeenCalledWith('some data'); 91 | expect($rootScope.$digest).toHaveBeenCalled(); 92 | }); 93 | 94 | it('binds a listener to the scope so it gets removed when the scope is destroyed', function() { 95 | // Arrange 96 | var callback = jasmine.createSpy(); 97 | 98 | // Act 99 | sut.once('foo', callback).bindTo($rootScope); 100 | $rootScope.$broadcast('$destroy'); 101 | $rootScope.$digest(); 102 | 103 | // Assert 104 | expect(socket.removeListener).toHaveBeenCalledWith('foo', currentCallback); 105 | }); 106 | }); 107 | 108 | describe('removeListener method', function() { 109 | it('removes a listener previously added', function() { 110 | // Arrange 111 | var callback = jasmine.createSpy(); 112 | sut.on('event', callback); 113 | 114 | // Act 115 | sut.removeListener('event', callback); 116 | 117 | // Assert 118 | expect(socket.removeListener).toHaveBeenCalledWith('event', currentCallback); 119 | }); 120 | }); 121 | 122 | describe('removeAllListeners method', function() { 123 | it('removes all listeners of an event', function() { 124 | // Act 125 | sut.removeAllListeners('event'); 126 | 127 | // Assert 128 | expect(socket.removeAllListeners).toHaveBeenCalledWith('event'); 129 | }); 130 | }); 131 | 132 | describe('emit method', function() { 133 | it('emits an event', function() { 134 | // Act 135 | sut.emit('event', 'some data'); 136 | 137 | // Assert 138 | expect(socket.emit).toHaveBeenCalledWith('event', 'some data'); 139 | }); 140 | 141 | it('emits an event with an acknowledgement callback', function() { 142 | // Arrange 143 | var callback = jasmine.createSpy(); 144 | 145 | // Act 146 | sut.emit('event', 'some data', callback); 147 | 148 | // Assert 149 | expect(socket.emit).toHaveBeenCalledWith('event', 'some data', jasmine.any(Function)); 150 | }); 151 | 152 | it('calls the acknowledgment callback and triggers a digest cycle', function() { 153 | // Arrange 154 | var callback = jasmine.createSpy(); 155 | sut.emit('event', 'some data', callback); 156 | 157 | // Act 158 | currentCallback('some other data'); 159 | 160 | // Assert 161 | expect(callback).toHaveBeenCalledWith('some other data'); 162 | expect($rootScope.$digest).toHaveBeenCalled(); 163 | }); 164 | }); 165 | }); 166 | 167 | }()); --------------------------------------------------------------------------------