├── .gitignore ├── README.md ├── bower.json ├── examples ├── example_1 │ ├── example_1.js │ └── index.html ├── example_2 │ ├── example_2.js │ └── index.html ├── example_3 │ ├── example_3.js │ └── index.html └── shared.js ├── firebase-resource.js ├── lib └── angular.js └── test ├── config └── karma.conf.js ├── firebase-resource-spec.js └── lib ├── angular-mocks.js └── custom-mocks.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.orig 2 | .DS_Store* 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | firebase-resource 2 | ================= 3 | 4 | Angular wrapper for Firebase in the style of ng-resource and active-record. 5 | 6 | *this is a very early release so please report any strangeness or suggestions* 7 | 8 | #### Features 9 | 10 | * Encapsulates Firebase to prevent dependency in controllers. Designed to be a drop-in replacement for ng-resource. 11 | * Handles associations using foreign-key references, following the tactics outlined [here](https://www.firebase.com/blog/2013-04-12-denormalizing-is-normal.html) 12 | * Automatically timestamps objects when saving and updating 13 | * Handles pagination 14 | * Stores resources in memory to prevent having to request data from Firebase as often 15 | * Allows assigning of local attributes. Any attribute with a leading underscore will *not* be persisted to Firebase 16 | 17 | planned features 18 | * Store resources in localStorage or indexedDB 19 | * Advanced querying options including searching 20 | * Remove reliance on Firebase and make it compatible with any realtime backend 21 | 22 | ####Getting Started 23 | Include firebase-resource.js in your index.html file. Suggest creating a services folder. 24 | 25 | 26 | 27 | firebase-resource requires a firebase module be injected into it. So define one thusly somewhere in your project: 28 | 29 | angular.module('exampleFirebase', []). 30 | value('firebase', (new Firebase('https://example.firebaseio.com/'))); 31 | 32 | firebase-resource relies on the existence of a safeApply method on $rootScope. In one of your controllers, define safeApply: 33 | 34 | $rootScope.safeApply = function(fn) { 35 | var phase = this.$root.$$phase; 36 | if(phase == '$apply' || phase == '$digest') { 37 | if(fn && (typeof(fn) === 'function')) { 38 | fn(); 39 | } 40 | } else { 41 | this.$apply(fn); 42 | } 43 | }; 44 | 45 | firebase-resource assumes you are defining your models as separate modules, much like ng-resource assumes. 46 | Inject firebase-resource into your where you might otherwise inject ng-resource. 47 | 48 | angular.module('Post', ['firebaseResource']). 49 | factory('Post', function (firebaseResource) { 50 | 51 | }); 52 | 53 | Create a resource using the resource factory defined in firebase-resource (again, like you would using ng-resource). 54 | 55 | angular.module('Post', ['firebaseResource']). 56 | factory('Post', function (firebaseResource) { 57 | 58 | var Post = firebaseResource( 59 | { 60 | path: 'posts' 61 | } 62 | ); 63 | 64 | return Post; 65 | 66 | }); 67 | 68 | Define options for the resource. These include 69 | 70 | * path - this is the actual Firebase path of the resource in question. 71 | * hasMany - an array of associations. These are other models defined as firebase-resources. 72 | * perPage - how many results are retrieved per page. 73 | * limit - used to limit number of results coming back from a query. Redundant when using pagination. 74 | * belongsTo - determines whether a resource is defined within the context of a parent resource. 75 | 76 | Inject the resource into controllers where needed 77 | 78 | angular.module('PostsCtrl', []). 79 | controller('PostsCtrl', function($scope, Post) { 80 | 81 | $scope.posts = Post.query({page: 1}); 82 | 83 | }); 84 | 85 | Defining and using associations: 86 | 87 | 88 | angular.module('User', ['firebaseResource']). 89 | factory('User', function (firebaseResource) { 90 | 91 | var User = firebaseResource( 92 | { 93 | path: 'users', 94 | hasMany: ['Post'] 95 | } 96 | ); 97 | 98 | return User; 99 | 100 | }); 101 | 102 | angular.module('Post', ['firebaseResource']). 103 | factory('Post', function (firebaseResource) { 104 | 105 | var Post = firebaseResource( 106 | { 107 | path: 'posts', 108 | belongsTo: true, 109 | perPage: 10 110 | } 111 | ); 112 | 113 | return Post; 114 | 115 | }); 116 | 117 | angular.module('PostsCtrl', []). 118 | controller('PostsCtrl', function($scope, Post, User) { 119 | 120 | User.query() // query() sets up the proper Firebase listeners for the model. Does not return the actual objects. 121 | $scope.user = User.all()[0]; // all() returns the actual objects pulled down from Firebase for this model 122 | $scope.user.posts().query({page: 1}); 123 | $scope.posts = $scope.user.posts().all(); 124 | 125 | }); 126 | 127 | 128 | Creating content in context of an assocation: 129 | 130 | var post = user.posts().new({content: 'hello world!'}); 131 | post.save(). 132 | then(function() { 133 | // do something 134 | }) 135 | 136 | Define lifecycle callbacks within the model definition: 137 | 138 | 139 | angular.module('Post', ['firebaseResource']). 140 | factory('Post', function (firebaseResource) { 141 | 142 | var Post = firebaseResource( 143 | { 144 | path: 'posts', 145 | } 146 | ); 147 | 148 | Post.prototype.init = function(error) { 149 | // happens when model is instantiated from Firebase data 150 | }; 151 | 152 | Post.prototype.afterCreate = function(error) { 153 | // happens only the first time an object is added to Firebase 154 | }; 155 | 156 | Post.prototype.beforeSave = function(error) { 157 | // happens immediately before saving data to Firebase 158 | }; 159 | 160 | Post.prototype.afterSave = function(error) { 161 | // happens after data is written to Firebase 162 | }; 163 | 164 | return Post; 165 | 166 | }); 167 | 168 | 169 | Instance Methods: 170 | 171 | * save() // saves resource to Firebase and returns a promise that is resolved upon success 172 | * delete() // delets resource from local stores and Firebase. Returns a promise that is resolved on success 173 | * getTimestamp(attr) // returns a javascript date of the provided timestamp. Accepts "created_at" or "updated_at" 174 | 175 | Class Methods: 176 | 177 | * getName() // returns the result of the firebase name() function 178 | * getPath() // returns the result of the firebase path() function 179 | * clearAll() // clears all resources out of memory 180 | * find(id) // returns a resource from memory for a given id, but will not go to Firebase for it 181 | * findAsync(id) // returns a resource from memory if available or Firebase if not. Returns a promise. 182 | * all() // returns all resources in memory. options include limit and parent. 183 | * query() // establishes firebase listeners for the resource given a set of options and stores results in resource stores. 184 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firebase-resource", 3 | "main": "firebase-resource.js", 4 | "version": "0.1.2", 5 | "homepage": "https://github.com/marknutter/firebase-resource", 6 | "authors": [ 7 | "Mark Nutter " 8 | ], 9 | "description": "An active-record inspired angularJS wrapper for Firebase", 10 | "keywords": [ 11 | "angular", 12 | "firebase" 13 | ], 14 | "license": "MIT", 15 | "ignore": [ 16 | "**/.*", 17 | "node_modules", 18 | "bower_components", 19 | "test", 20 | "tests" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /examples/example_1/example_1.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | angular.module('Foo', ['firebaseResource']). 4 | factory('Foo', function ($rootScope, firebaseResource) { 5 | var Foo = firebaseResource( 6 | { 7 | path: 'foos' 8 | } 9 | ); 10 | return Foo; 11 | }); 12 | 13 | 14 | angular.module('MainCtrl', ['Foo']). 15 | controller('MainCtrl', function($rootScope, $scope, Foo) { 16 | 17 | var foo1 = new Foo({name: 'foo1'}); 18 | foo1.save(); 19 | 20 | Foo.query({page: 1}) 21 | 22 | $scope.foos = Foo.all(); 23 | 24 | 25 | }) 26 | 27 | -------------------------------------------------------------------------------- /examples/example_1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /examples/example_2/example_2.js: -------------------------------------------------------------------------------- 1 | 2 | angular.module('User', ['firebaseResource']). 3 | factory('User', function ($rootScope, firebaseResource) { 4 | var User = firebaseResource( 5 | { 6 | path: 'users', 7 | hasMany: ['Post'] 8 | } 9 | ); 10 | return User; 11 | }); 12 | 13 | angular.module('Post', ['firebaseResource']). 14 | factory('Post', function ($rootScope, firebaseResource) { 15 | var Post = firebaseResource( 16 | { 17 | path: 'posts', 18 | belongsTo: true 19 | } 20 | ); 21 | return Post; 22 | }); 23 | 24 | 25 | angular.module('MainCtrl', ['User', 'Post']). 26 | controller('MainCtrl', function($rootScope, $scope, User, Post) { 27 | 28 | $scope.user = new User({name: 'Test User'}); 29 | $scope.user.save() 30 | 31 | $scope.user.posts().query(); 32 | 33 | var post1 = $scope.user.posts().new({content: 'test post 1'}); 34 | post1.save(); 35 | var post2 = $scope.user.posts().new({content: 'test post 2'}); 36 | post2.save() 37 | 38 | $scope.addPost = function() { 39 | var newPost = $scope.user.posts().new({content: 'another post!'}); 40 | newPost.save() 41 | } 42 | 43 | }) 44 | 45 | -------------------------------------------------------------------------------- /examples/example_2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

{{user.name}} posts

9 | 10 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/example_3/example_3.js: -------------------------------------------------------------------------------- 1 | 2 | angular.module('User', ['firebaseResource']). 3 | factory('User', function ($rootScope, firebaseResource) { 4 | var User = firebaseResource( 5 | { 6 | path: 'users', 7 | hasMany: ['Post'] 8 | } 9 | ); 10 | return User; 11 | }); 12 | 13 | angular.module('Post', ['firebaseResource']). 14 | factory('Post', function ($rootScope, firebaseResource) { 15 | var Post = firebaseResource( 16 | { 17 | path: 'posts', 18 | belongsTo: true, 19 | perPage: 3 20 | } 21 | ); 22 | return Post; 23 | }); 24 | 25 | 26 | angular.module('MainCtrl', ['User', 'Post']). 27 | controller('MainCtrl', function($rootScope, $scope, User, Post) { 28 | 29 | $scope.user = new User({name: 'Test User'}); 30 | $scope.user.save() 31 | $scope.current_page = 0; 32 | 33 | /* 34 | * Pagination is a way to limit the number of objects coming back 35 | * from Firebase for a given query. It is not intended to only show 36 | * a certain number of objects in a given view; that will still need 37 | * to be handled separatley. So whenever a new page is requested via 38 | * the query() method, those objects are added to the full list of 39 | * objects. For now, pagination is in reverse order, so the most 40 | * recently created objects come back with page 1. 41 | */ 42 | 43 | for (var i=0;i<10;i++) { 44 | var post = $scope.user.posts().new({content: 'test post ' + i}); 45 | post.save().then(function() { 46 | Post.clearAll(); // clearing all cached objects so that pagination works for this demo 47 | }); 48 | } 49 | 50 | $scope.nextPage = function() { 51 | $scope.current_page++; 52 | $scope.user.posts().query({page: $scope.current_page}); 53 | } 54 | 55 | 56 | }) 57 | 58 | -------------------------------------------------------------------------------- /examples/example_3/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

{{user.name}} posts

9 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/shared.js: -------------------------------------------------------------------------------- 1 | angular.module('app', ['MainCtrl', 'fbrFirebase']) 2 | .run(function($rootScope, firebase) { 3 | 4 | // reset the database every time 5 | firebase.remove(); 6 | 7 | $rootScope.safeApply = function(fn) { 8 | var phase = this.$root.$$phase; 9 | if(phase == '$apply' || phase == '$digest') { 10 | if(fn && (typeof(fn) === 'function')) { 11 | fn(); 12 | } 13 | } else { 14 | this.$apply(fn); 15 | } 16 | }; 17 | }); 18 | 19 | angular.module('fbrFirebase', []). 20 | value('firebase', (new Firebase('https://fbr.firebaseio.com/'))); -------------------------------------------------------------------------------- /firebase-resource.js: -------------------------------------------------------------------------------- 1 | angular.module('firebaseResource', []). 2 | 3 | factory('firebaseResource', function($injector, $rootScope, $log, $timeout, $filter, $q, firebase) { 4 | 5 | 6 | $rootScope.safeApply = function(fn) { 7 | var phase = this.$root.$$phase; 8 | if(phase == '$apply' || phase == '$digest') { 9 | if(fn && (typeof(fn) === 'function')) { 10 | fn(); 11 | } 12 | } else { 13 | this.$apply(fn); 14 | } 15 | }; 16 | 17 | function firebaseResourceFactory(opts) { 18 | var options = opts ? opts : {}; 19 | var map = {}; 20 | var list = []; 21 | var listenerPaths = {}; 22 | options.perPage = opts.perPage ? opts.perPage : 10; 23 | var globalLimit = opts.limit ? opts.limit : 1000; 24 | if (opts.path) { 25 | var resourceRef = firebase.child(opts.path); 26 | var resourcePath = firebase.path; 27 | // var resourceQuery = firebase.child(opts.path).limit(globalLimit); 28 | if (!opts.belongsTo) { 29 | setListeners(opts.path, options); 30 | } 31 | } 32 | 33 | function Resource(data) { 34 | angular.copy(data || {}, this); 35 | var _this = this; 36 | if (this.id) { 37 | setAssociations(this) 38 | }; 39 | } 40 | 41 | /***** Private Methods *****/ 42 | 43 | function setAssociations(self) { 44 | var _this = self; 45 | angular.forEach(options.hasMany, function(model) { 46 | model = $injector.get(model); 47 | var parent_path = Resource.getPath() + "/" + _this.id; 48 | var parent_rels_path = Resource.getPath() + "/" + _this.id + "/rels/" + model.getName() + "/"; 49 | if (parent_path && !_this[model.getName()]) { 50 | _this["_" + model.getName()] = []; 51 | 52 | _this[model.getName()] = function() { 53 | return { 54 | query: function(opts, callback) { 55 | var opts = opts ? opts : {}; 56 | opts.path = parent_rels_path; 57 | opts.parent = _this; 58 | return model.query(opts, callback); 59 | }, 60 | all: function(opts) { 61 | var opts = opts ? opts : {}; 62 | opts.parent = _this; 63 | return model.all(opts); 64 | }, 65 | new: function(data) { 66 | var data = data ? data : {} 67 | data["_" + Resource.getName() + "_parent_id"] = _this.id; 68 | data._parent_path = parent_path; 69 | data._parent_rels_path = parent_rels_path; 70 | return new model(data); 71 | }, 72 | add: function(obj) { 73 | var deferred = $q.defer(); 74 | if (!obj._parent_rels_path) { 75 | firebase.child(parent_rels_path + obj.id).once('value', function(s) { 76 | if (s.val()) { 77 | deferred.resolve(_this); 78 | } else { 79 | firebase.child(parent_rels_path).once('value', function(parentRels) { 80 | var priority = parentRels.val() ? Object.keys(parentRels.val()).length : 1; 81 | firebase.child(parent_rels_path + obj.id).setWithPriority(true, priority, function(error) { 82 | if (error) { 83 | $log.info('something went wrong: ' + error); 84 | } else { 85 | deferred.resolve(_this); 86 | $rootScope.safeApply(); 87 | } 88 | }); 89 | }); 90 | } 91 | }) 92 | } 93 | return deferred.promise; 94 | } 95 | }; 96 | }; 97 | }; 98 | }); 99 | } 100 | 101 | function ensureRels(obj, relName) { 102 | if (!obj.rels) { 103 | obj.rels = {}; 104 | } 105 | if (!obj.rels[relName]) { 106 | obj.rels[relName] = {}; 107 | } 108 | } 109 | 110 | function getPagingQuery(parent, path, page) { 111 | var perPage = options.perPage; 112 | if (parent && page) { 113 | ensureRels(parent, Resource.getName()); 114 | var total = Object.keys(parent.rels[Resource.getName()]).length; 115 | var end = total-perPage*(page-1); 116 | var start = total-perPage*page; 117 | $log.info("start: " + start + ", end: " + end); 118 | start = start < 1 ? 1 : start; // start cannot be less than 1; 119 | end = end < start ? start : end; // end cannot be less than start; 120 | if (page == 1) { 121 | var query = firebase.child(path).startAt(start); 122 | $log.info('no end'); 123 | } else { 124 | var query = firebase.child(path).endAt(end).startAt(start); 125 | } 126 | 127 | path += "?page=" + page; 128 | $log.info("start: " + start + ", end: " + end + ", path:" +path); 129 | return query; 130 | } else { 131 | return false; 132 | } 133 | } 134 | 135 | function setListeners(path, opts, callback) { 136 | var opts = opts ? opts : {}; 137 | var query = getPagingQuery(opts.parent, path, opts.page); 138 | if (!query) { query = firebase.child(path); }; 139 | if (opts.page) { path += "?page=" + opts.page; }; 140 | if (!listenerPaths[path]) { 141 | listenerPaths[path] = true; 142 | 143 | query.on('child_added', function(snapshot) { 144 | $log.info('child_added'); 145 | 146 | if (opts.parent) { 147 | firebase.child(Resource.getPath() + "/" + snapshot.name()).once('value', function(snap) { 148 | if (snap.val()) { 149 | var resource = updateResource(snap); 150 | resource.init(); 151 | // add local variable for parent for filtering purposes. 152 | resource["_" + opts.parent.getName() + "_parent_id"] = opts.parent.id; 153 | resource._parent_path = opts.parent.getName() + "/" + opts.parent.id; 154 | resource._parent_rels_path = opts.parent.getName() + "/" + opts.parent.id + "/rels/" + Resource.getName() + "/"; 155 | refreshRels(opts.parent); 156 | if (callback) { 157 | callback(resource); 158 | } 159 | } 160 | }) 161 | } else { 162 | var resource = updateResource(snapshot); 163 | resource.init(); 164 | $rootScope.safeApply(); 165 | } 166 | }); 167 | 168 | resourceRef.on('child_removed', function(snapshot) { 169 | $log.info('child_removed'); 170 | removeResource(snapshot); 171 | if (opts.parent) { 172 | refreshRels(opts.parent); 173 | } 174 | $rootScope.safeApply(); 175 | }); 176 | 177 | resourceRef.on('child_changed', function(snapshot) { 178 | $log.info('child_changed'); 179 | updateResource(snapshot); 180 | $rootScope.safeApply(); 181 | }); 182 | 183 | } 184 | } 185 | 186 | function removeResource(snapshot) { 187 | var name = snapshot.name ? snapshot.name() : snapshot.id; 188 | if (map[name]) { 189 | var index = list.indexOf(map[name]); 190 | list.splice(index, 1); 191 | delete map[name]; 192 | } 193 | } 194 | 195 | function updateResource(snapshot) { 196 | var name = snapshot.name(); 197 | var data = snapshot.val(); 198 | if (data) { 199 | if (map[name]) { 200 | angular.forEach(data, function(val, key) { 201 | map[name][key] = val; 202 | }); 203 | var resource = map[name]; 204 | } else { 205 | data.id = name; 206 | var resource = new Resource(data); 207 | addResource(resource); 208 | } 209 | return resource; 210 | } 211 | } 212 | 213 | function addResource(resource) { 214 | map[resource.id] = resource; 215 | var listIndex = indexInList(resource.id); 216 | if (listIndex === -1) { 217 | list.push(map[resource.id]) 218 | } else { 219 | list[listIndex] = map[resource.id]; 220 | }; 221 | } 222 | 223 | function refreshRels(parent) { 224 | var filterParams = {}; 225 | filterParams["_" + parent.getName() + "_parent_id"] = parent.id; 226 | parent["_" + Resource.getName()] = $filter('filter')(list, filterParams); 227 | $rootScope.safeApply(); 228 | } 229 | 230 | 231 | 232 | function indexInList(id) { 233 | var index = -1; 234 | for (var i in list) { 235 | if (list[i].id === id) { 236 | index = i; 237 | } 238 | } 239 | return index; 240 | } 241 | 242 | function timestamp(resource) { 243 | resource.updated_at = Firebase.ServerValue.TIMESTAMP 244 | if (!resource.created_at) { 245 | resource.created_at = Firebase.ServerValue.TIMESTAMP 246 | } 247 | } 248 | 249 | function getSaveableAttrs(resource) { 250 | var toSave = {}; 251 | for (var key in resource) { 252 | if (typeof(resource[key]) !== "function" && 253 | key.charAt(0) !== "$" && 254 | key !== "rels" && 255 | key.charAt(0) !== "_") { 256 | toSave[key] = resource[key]; 257 | } 258 | }; 259 | return toSave; 260 | } 261 | 262 | 263 | /***** Class Methods *****/ 264 | 265 | 266 | Resource.getName = function() { 267 | return resourceRef.name(); 268 | } 269 | 270 | Resource.getPath = function() { 271 | return resourceRef.path.toString(); 272 | } 273 | 274 | Resource.clearAll = function() { 275 | map = {}; 276 | list = []; 277 | } 278 | 279 | Resource.find = function(id) { 280 | return map[id]; 281 | } 282 | 283 | Resource.findAsync = function(id) { 284 | var deferred = $q.defer(); 285 | if (map[id]) { 286 | $timeout(function() { 287 | $rootScope.safeApply(function() { 288 | deferred.resolve(map[id]); 289 | }) 290 | }, 0) 291 | } else { 292 | resourceRef.child(id).once('value', function(snapshot) { 293 | var resource = updateResource(snapshot); 294 | map[id] = resource; 295 | $rootScope.safeApply(function() { 296 | deferred.resolve(map[id]); 297 | }); 298 | }); 299 | } 300 | 301 | return deferred.promise; 302 | } 303 | 304 | Resource.all = function(opts) { 305 | var opts = opts ? opts : {} 306 | var ret = list; 307 | if (opts.parent) { 308 | ret = opts.parent["_" + Resource.getName()]; 309 | } 310 | if (opts.limit && opts.limit <= ret.length) { 311 | ret = ret.slice(ret.length - opts.limit, ret.length); 312 | } 313 | return ret; 314 | } 315 | 316 | Resource.query = function(opts, callback) { 317 | var opts = opts ? opts : {} 318 | var ret = list; 319 | var path = opts.path ? opts.path : Resource.getPath(); 320 | setListeners(path, opts, callback); 321 | } 322 | 323 | 324 | /***** Instance Methods *****/ 325 | 326 | 327 | Resource.prototype.getName = function() { 328 | return Resource.getName(); 329 | } 330 | 331 | Resource.prototype.getTimestamp = function(attr) { 332 | var date = new Date(this[attr]); 333 | return date.getTime(); 334 | } 335 | 336 | 337 | 338 | Resource.prototype.save = function() { 339 | this.beforeSave(); 340 | timestamp(this); 341 | 342 | var deferred = $q.defer(), 343 | newResource = false, 344 | _this = this, 345 | toSave = getSaveableAttrs(this), 346 | ref = this.id ? resourceRef.child(this.id) : resourceRef.push(); 347 | 348 | if (!this.id) { 349 | this.id = ref.name(); 350 | setAssociations(this); 351 | newResource = true; 352 | } 353 | 354 | ref.update(toSave, function(error) { 355 | $rootScope.safeApply(function() { 356 | if (error) { 357 | deferred.reject(error); 358 | } else { 359 | // addResource(_this); 360 | if (_this._parent_rels_path && newResource) { 361 | firebase.child(_this._parent_rels_path).once('value', function(parentRels) { 362 | var priority = parentRels.val() ? Object.keys(parentRels.val()).length : 1; 363 | firebase.child(_this._parent_rels_path + _this.id).setWithPriority(true, priority); 364 | }); 365 | }; 366 | 367 | _this.afterSave(); 368 | if (newResource) { 369 | _this.afterCreate(); 370 | }; 371 | deferred.resolve(_this); 372 | }; 373 | }); 374 | }); 375 | 376 | return deferred.promise; 377 | } 378 | 379 | Resource.prototype.delete = function() { 380 | 381 | if (this.id) { 382 | var ref = resourceRef.child(this.id); 383 | ref.remove(); 384 | if (this._parent_rels_path) { 385 | firebase.child(this._parent_rels_path + this.id).remove(); 386 | } 387 | } 388 | } 389 | 390 | 391 | // lifecycle callbacks - override in model 392 | Resource.prototype.init = function() { $log.info('initializing resource') } 393 | Resource.prototype.beforeSave = function() { $log.info('before save resource') } 394 | Resource.prototype.afterSave = function() { $log.info('after save resource') } 395 | Resource.prototype.afterCreate = function() { $log.info('after create resource') } 396 | 397 | 398 | return Resource; 399 | 400 | } 401 | 402 | return firebaseResourceFactory; 403 | 404 | }); 405 | -------------------------------------------------------------------------------- /test/config/karma.conf.js: -------------------------------------------------------------------------------- 1 | basePath = '../../'; 2 | 3 | files = [ 4 | JASMINE, 5 | JASMINE_ADAPTER, 6 | 'lib/angular.js', 7 | 'test/lib/angular-mocks.js', 8 | 'test/lib/custom-mocks.js', 9 | 'firebase-resource.js', 10 | 'test/firebase-resource-spec.js' 11 | ]; 12 | 13 | // server port 14 | port = 8081; 15 | 16 | autoWatch = true; 17 | 18 | // runner port 19 | runnerPort = 9100; 20 | 21 | browsers = ['PhantomJS']; 22 | 23 | reporters = ['dots']; 24 | -------------------------------------------------------------------------------- /test/firebase-resource-spec.js: -------------------------------------------------------------------------------- 1 | describe('Service: firebaseResource', function () { 2 | 'use strict'; 3 | var firebaseResource, 4 | Model, 5 | NestedModel, 6 | Bar, 7 | instance, 8 | snapshot, 9 | resource, 10 | firebase = customMocks.Firebase.new, 11 | scope; 12 | 13 | 14 | 15 | 16 | firebase = new Firebase(); 17 | 18 | beforeEach(module('firebaseResource')); 19 | 20 | beforeEach(function() { 21 | 22 | Bar = function(data) { angular.copy(data || {}, this) } 23 | Bar.getName = jasmine.createSpy('getName').andReturn('bars') 24 | Bar.all = jasmine.createSpy('all') 25 | Bar.new = jasmine.createSpy('new') 26 | 27 | module(function($provide) { 28 | $provide.value('firebase', firebase); 29 | $provide.value('Bar', Bar) 30 | $provide.value('$rootScope', customMocks.rootScope) 31 | $provide.value('$q', customMocks.q) 32 | }); 33 | 34 | inject(function($injector) { 35 | firebaseResource = $injector.get('firebaseResource'); 36 | }); 37 | 38 | }); 39 | 40 | describe('on initialization of a nested resource', function() { 41 | 42 | beforeEach(function() { 43 | 44 | firebase.child.reset(); 45 | Model = firebaseResource( 46 | { 47 | path: 'foos', 48 | limit: 15, 49 | hasMany: ['Bar'], 50 | belongsTo: true 51 | } 52 | ) 53 | 54 | resource = new Model({id: 1}); 55 | 56 | }); 57 | 58 | it('should implement getName()', function() { 59 | expect(Model.getName()).toBe("test") 60 | }) 61 | 62 | it('should implement getPath()', function() { 63 | expect(Model.getPath()).toBe("/test"); 64 | }) 65 | 66 | it('should implement getPath()', function() { 67 | expect(Model.getPath()).toBe("/test"); 68 | }) 69 | 70 | it('should get all() from Bar within context of Foo', function() { 71 | resource.bars().all(); 72 | expect(Bar.all).toHaveBeenCalledWith( 73 | { parent: jasmine.any(Object) 74 | }) 75 | }); 76 | 77 | 78 | describe('when calling all() on a nested resource', function() { 79 | beforeEach(function() { 80 | firebase.child.reset(); 81 | firebase.on.reset(); 82 | Firebase.prototype.name = function() { return 'bars' } 83 | Bar = firebaseResource({ 84 | path: 'bars', 85 | limit: 15, 86 | belongsTo: true 87 | }) 88 | 89 | Bar.query({ path: '/test/1/rels/bars/', 90 | parent: resource 91 | }); 92 | 93 | snapshot = { 94 | name: function() { return "2"}, 95 | val: function() { 96 | return this.data 97 | }, 98 | data: { 99 | bing: 'bow', 100 | id: 2, 101 | bars_parent_id: 1 102 | } 103 | } 104 | 105 | }) 106 | 107 | it('should set listeners with respect to parent association', function() { 108 | expect(firebase.child).toHaveBeenCalledWith('bars'); 109 | expect(firebase.on.calls.length).toBe(3); 110 | expect(firebase.on.calls[0].args[0]).toBe('child_added', Function); 111 | expect(firebase.on.calls[1].args[0]).toBe('child_removed', Function); 112 | expect(firebase.on.calls[2].args[0]).toBe('child_changed', Function); 113 | }); 114 | 115 | it('should add nested resource to parent resource on child_added event', function() { 116 | firebase.child.reset(); 117 | customMocks.Firebase.on_child_added(snapshot); 118 | expect(firebase.child).toHaveBeenCalledWith('/test/2'); 119 | expect(firebase.once).toHaveBeenCalledWith('value', jasmine.any(Function)); 120 | customMocks.Firebase.once(snapshot); 121 | expect(resource._bars[0]).toBe(Bar.find(2)) 122 | expect(Bar.find(2).bing).toEqual('bow'); 123 | expect(Bar.find(2).id).toEqual('2'); 124 | expect(Bar.find(2).bars_parent_id).toEqual(1); 125 | }) 126 | 127 | }) 128 | 129 | 130 | describe('an instantiation', function() { 131 | 132 | it('should allow instantiation from Bar within context of Foo', function() { 133 | var newBar = resource.bars().new({bing: 'bow'}); 134 | expect(newBar).toEqual({ 135 | bing: 'bow', 136 | _bars_parent_id: 1, 137 | _parent_path: '/test/1', 138 | _parent_rels_path : '/test/1/rels/bars/' 139 | }) 140 | }); 141 | 142 | it('should allow the adding of an existing resource to a parent', function() { 143 | var existingBar = new Bar({id: 2, bing: 'bow'}); 144 | resource.bars().add(existingBar); 145 | expect(firebase.child).toHaveBeenCalledWith('foos') 146 | }) 147 | 148 | 149 | 150 | }); 151 | 152 | }); 153 | 154 | describe('on initialization of a standalone resource', function() { 155 | 156 | beforeEach(function() { 157 | Firebase.prototype.name = function() { return 'foos' } 158 | firebase.on.reset(); 159 | Model = firebaseResource( 160 | { 161 | path: 'foos', 162 | limit: 15 163 | } 164 | ) 165 | 166 | snapshot = { 167 | name: function() { return "1"}, 168 | val: function() { 169 | return this.data 170 | }, 171 | data: { 172 | foo: 'bar', 173 | id: 1 174 | } 175 | } 176 | 177 | 178 | }); 179 | 180 | it('should set listeners', function() { 181 | expect(firebase.child).toHaveBeenCalledWith('foos'); 182 | expect(firebase.on.calls.length).toBe(3); 183 | expect(firebase.on.calls[0].args[0]).toBe('child_added', Function); 184 | expect(firebase.on.calls[1].args[0]).toBe('child_removed', Function); 185 | expect(firebase.on.calls[2].args[0]).toBe('child_changed', Function); 186 | }) 187 | 188 | it('should add a resource on child_added event', function() { 189 | expect(Model.all().length).toBe(0); 190 | customMocks.Firebase.on_child_added(snapshot); 191 | expect(Model.all().length).toBe(1); 192 | }) 193 | 194 | it('should update a resource on child_changed event', function() { 195 | customMocks.Firebase.on_child_added(snapshot); 196 | expect(Model.find(1).foo).toBe('bar'); 197 | snapshot.data.foo = "baz"; 198 | customMocks.Firebase.on_child_changed(snapshot); 199 | expect(Model.find(1).foo).toBe('baz'); 200 | expect(Model.all().length).toBe(1); 201 | }) 202 | 203 | it('should remove a resource on child_removed event', function() { 204 | customMocks.Firebase.on_child_added(snapshot); 205 | expect(Model.all().length).toBe(1); 206 | customMocks.Firebase.on_child_removed(snapshot); 207 | expect(Model.all().length).toBe(0); 208 | }) 209 | 210 | it('should find a resource asynchronously', function() { 211 | var ref = Model.findAsync(1); 212 | expect(Model.find(1)).toBe(undefined); 213 | expect(firebase.once).toHaveBeenCalled(); 214 | expect(ref.then).toEqual(jasmine.any(Function)); 215 | customMocks.Firebase.once(snapshot); 216 | expect(Model.find(1).foo).toBe('bar'); 217 | 218 | firebase.once.reset(); 219 | customMocks.Firebase.on_child_added(snapshot); 220 | Model.findAsync(1); 221 | expect(firebase.once).not.toHaveBeenCalled(); 222 | expect(Model.find(1).foo).toBe('bar'); 223 | }) 224 | 225 | describe('an instantiation', function() { 226 | 227 | beforeEach(function() { 228 | resource = { 229 | foo: 'bar', 230 | fun: angular.noop, 231 | _local: 'value' 232 | } 233 | instance = new Model(resource); 234 | }) 235 | 236 | it('should contain any data passed in', function () { 237 | expect(instance.foo).toBe('bar'); 238 | }) 239 | 240 | it('should contain any data passed in', function () { 241 | expect(instance.foo).toBe('bar'); 242 | }) 243 | 244 | it('should respond to getName()', function () { 245 | expect(instance.getName()).toBe('foos'); 246 | }) 247 | 248 | it('should be saveable', function () { 249 | 250 | expect(instance.id).toBe(undefined); 251 | expect(instance.created_at).toBe(undefined) 252 | instance.afterCreate = jasmine.createSpy('afterCreate'); 253 | instance.beforeSave = jasmine.createSpy('beforeSave'); 254 | 255 | instance.save(); 256 | expect(firebase.update).toHaveBeenCalledWith( 257 | { 258 | foo: 'bar', 259 | created_at: instance.created_at, 260 | updated_at: instance.updated_at, 261 | }, 262 | jasmine.any(Function) 263 | ); 264 | expect(instance.created_at).not.toBe(undefined); 265 | expect(instance.updated_at).not.toBe(undefined); 266 | expect(instance.afterCreate).toHaveBeenCalled(); 267 | expect(instance.beforeSave).toHaveBeenCalled(); 268 | expect(instance.id).toBe('foos'); 269 | expect(customMocks.deferred.resolve).toHaveBeenCalled(); 270 | customMocks.deferred.resolve.reset(); 271 | }); 272 | 273 | it('should be deletable', function() { 274 | instance.save(); 275 | instance.delete(); 276 | expect(firebase.remove).toHaveBeenCalled(); 277 | }); 278 | 279 | 280 | it('should respond to getTimestamp() and return adjusted time', function() { 281 | var created_at = Date.now(); 282 | instance.created_at = created_at 283 | expect(instance.getTimestamp('created_at')).toEqual(created_at) 284 | }) 285 | 286 | }); 287 | 288 | }); 289 | 290 | 291 | }); -------------------------------------------------------------------------------- /test/lib/angular-mocks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.0.7 3 | * (c) 2010-2012 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | * 6 | * TODO(vojta): wrap whole file into closure during build 7 | */ 8 | 9 | /** 10 | * @ngdoc overview 11 | * @name angular.mock 12 | * @description 13 | * 14 | * Namespace from 'angular-mocks.js' which contains testing related code. 15 | */ 16 | angular.mock = {}; 17 | 18 | /** 19 | * ! This is a private undocumented service ! 20 | * 21 | * @name ngMock.$browser 22 | * 23 | * @description 24 | * This service is a mock implementation of {@link ng.$browser}. It provides fake 25 | * implementation for commonly used browser apis that are hard to test, e.g. setTimeout, xhr, 26 | * cookies, etc... 27 | * 28 | * The api of this service is the same as that of the real {@link ng.$browser $browser}, except 29 | * that there are several helper methods available which can be used in tests. 30 | */ 31 | angular.mock.$BrowserProvider = function() { 32 | this.$get = function(){ 33 | return new angular.mock.$Browser(); 34 | }; 35 | }; 36 | 37 | angular.mock.$Browser = function() { 38 | var self = this; 39 | 40 | this.isMock = true; 41 | self.$$url = "http://server/"; 42 | self.$$lastUrl = self.$$url; // used by url polling fn 43 | self.pollFns = []; 44 | 45 | // TODO(vojta): remove this temporary api 46 | self.$$completeOutstandingRequest = angular.noop; 47 | self.$$incOutstandingRequestCount = angular.noop; 48 | 49 | 50 | // register url polling fn 51 | 52 | self.onUrlChange = function(listener) { 53 | self.pollFns.push( 54 | function() { 55 | if (self.$$lastUrl != self.$$url) { 56 | self.$$lastUrl = self.$$url; 57 | listener(self.$$url); 58 | } 59 | } 60 | ); 61 | 62 | return listener; 63 | }; 64 | 65 | self.cookieHash = {}; 66 | self.lastCookieHash = {}; 67 | self.deferredFns = []; 68 | self.deferredNextId = 0; 69 | 70 | self.defer = function(fn, delay) { 71 | delay = delay || 0; 72 | self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId}); 73 | self.deferredFns.sort(function(a,b){ return a.time - b.time;}); 74 | return self.deferredNextId++; 75 | }; 76 | 77 | 78 | self.defer.now = 0; 79 | 80 | 81 | self.defer.cancel = function(deferId) { 82 | var fnIndex; 83 | 84 | angular.forEach(self.deferredFns, function(fn, index) { 85 | if (fn.id === deferId) fnIndex = index; 86 | }); 87 | 88 | if (fnIndex !== undefined) { 89 | self.deferredFns.splice(fnIndex, 1); 90 | return true; 91 | } 92 | 93 | return false; 94 | }; 95 | 96 | 97 | /** 98 | * @name ngMock.$browser#defer.flush 99 | * @methodOf ngMock.$browser 100 | * 101 | * @description 102 | * Flushes all pending requests and executes the defer callbacks. 103 | * 104 | * @param {number=} number of milliseconds to flush. See {@link #defer.now} 105 | */ 106 | self.defer.flush = function(delay) { 107 | if (angular.isDefined(delay)) { 108 | self.defer.now += delay; 109 | } else { 110 | if (self.deferredFns.length) { 111 | self.defer.now = self.deferredFns[self.deferredFns.length-1].time; 112 | } else { 113 | throw Error('No deferred tasks to be flushed'); 114 | } 115 | } 116 | 117 | while (self.deferredFns.length && self.deferredFns[0].time <= self.defer.now) { 118 | self.deferredFns.shift().fn(); 119 | } 120 | }; 121 | /** 122 | * @name ngMock.$browser#defer.now 123 | * @propertyOf ngMock.$browser 124 | * 125 | * @description 126 | * Current milliseconds mock time. 127 | */ 128 | 129 | self.$$baseHref = ''; 130 | self.baseHref = function() { 131 | return this.$$baseHref; 132 | }; 133 | }; 134 | angular.mock.$Browser.prototype = { 135 | 136 | /** 137 | * @name ngMock.$browser#poll 138 | * @methodOf ngMock.$browser 139 | * 140 | * @description 141 | * run all fns in pollFns 142 | */ 143 | poll: function poll() { 144 | angular.forEach(this.pollFns, function(pollFn){ 145 | pollFn(); 146 | }); 147 | }, 148 | 149 | addPollFn: function(pollFn) { 150 | this.pollFns.push(pollFn); 151 | return pollFn; 152 | }, 153 | 154 | url: function(url, replace) { 155 | if (url) { 156 | this.$$url = url; 157 | return this; 158 | } 159 | 160 | return this.$$url; 161 | }, 162 | 163 | cookies: function(name, value) { 164 | if (name) { 165 | if (value == undefined) { 166 | delete this.cookieHash[name]; 167 | } else { 168 | if (angular.isString(value) && //strings only 169 | value.length <= 4096) { //strict cookie storage limits 170 | this.cookieHash[name] = value; 171 | } 172 | } 173 | } else { 174 | if (!angular.equals(this.cookieHash, this.lastCookieHash)) { 175 | this.lastCookieHash = angular.copy(this.cookieHash); 176 | this.cookieHash = angular.copy(this.cookieHash); 177 | } 178 | return this.cookieHash; 179 | } 180 | }, 181 | 182 | notifyWhenNoOutstandingRequests: function(fn) { 183 | fn(); 184 | } 185 | }; 186 | 187 | 188 | /** 189 | * @ngdoc object 190 | * @name ngMock.$exceptionHandlerProvider 191 | * 192 | * @description 193 | * Configures the mock implementation of {@link ng.$exceptionHandler} to rethrow or to log errors passed 194 | * into the `$exceptionHandler`. 195 | */ 196 | 197 | /** 198 | * @ngdoc object 199 | * @name ngMock.$exceptionHandler 200 | * 201 | * @description 202 | * Mock implementation of {@link ng.$exceptionHandler} that rethrows or logs errors passed 203 | * into it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration 204 | * information. 205 | * 206 | * 207 | *
 208 |  *   describe('$exceptionHandlerProvider', function() {
 209 |  *
 210 |  *     it('should capture log messages and exceptions', function() {
 211 |  *
 212 |  *       module(function($exceptionHandlerProvider) {
 213 |  *         $exceptionHandlerProvider.mode('log');
 214 |  *       });
 215 |  *
 216 |  *       inject(function($log, $exceptionHandler, $timeout) {
 217 |  *         $timeout(function() { $log.log(1); });
 218 |  *         $timeout(function() { $log.log(2); throw 'banana peel'; });
 219 |  *         $timeout(function() { $log.log(3); });
 220 |  *         expect($exceptionHandler.errors).toEqual([]);
 221 |  *         expect($log.assertEmpty());
 222 |  *         $timeout.flush();
 223 |  *         expect($exceptionHandler.errors).toEqual(['banana peel']);
 224 |  *         expect($log.log.logs).toEqual([[1], [2], [3]]);
 225 |  *       });
 226 |  *     });
 227 |  *   });
 228 |  * 
229 | */ 230 | 231 | angular.mock.$ExceptionHandlerProvider = function() { 232 | var handler; 233 | 234 | /** 235 | * @ngdoc method 236 | * @name ngMock.$exceptionHandlerProvider#mode 237 | * @methodOf ngMock.$exceptionHandlerProvider 238 | * 239 | * @description 240 | * Sets the logging mode. 241 | * 242 | * @param {string} mode Mode of operation, defaults to `rethrow`. 243 | * 244 | * - `rethrow`: If any errors are passed into the handler in tests, it typically 245 | * means that there is a bug in the application or test, so this mock will 246 | * make these tests fail. 247 | * - `log`: Sometimes it is desirable to test that an error is thrown, for this case the `log` mode stores an 248 | * array of errors in `$exceptionHandler.errors`, to allow later assertion of them. 249 | * See {@link ngMock.$log#assertEmpty assertEmpty()} and 250 | * {@link ngMock.$log#reset reset()} 251 | */ 252 | this.mode = function(mode) { 253 | switch(mode) { 254 | case 'rethrow': 255 | handler = function(e) { 256 | throw e; 257 | }; 258 | break; 259 | case 'log': 260 | var errors = []; 261 | 262 | handler = function(e) { 263 | if (arguments.length == 1) { 264 | errors.push(e); 265 | } else { 266 | errors.push([].slice.call(arguments, 0)); 267 | } 268 | }; 269 | 270 | handler.errors = errors; 271 | break; 272 | default: 273 | throw Error("Unknown mode '" + mode + "', only 'log'/'rethrow' modes are allowed!"); 274 | } 275 | }; 276 | 277 | this.$get = function() { 278 | return handler; 279 | }; 280 | 281 | this.mode('rethrow'); 282 | }; 283 | 284 | 285 | /** 286 | * @ngdoc service 287 | * @name ngMock.$log 288 | * 289 | * @description 290 | * Mock implementation of {@link ng.$log} that gathers all logged messages in arrays 291 | * (one array per logging level). These arrays are exposed as `logs` property of each of the 292 | * level-specific log function, e.g. for level `error` the array is exposed as `$log.error.logs`. 293 | * 294 | */ 295 | angular.mock.$LogProvider = function() { 296 | 297 | function concat(array1, array2, index) { 298 | return array1.concat(Array.prototype.slice.call(array2, index)); 299 | } 300 | 301 | 302 | this.$get = function () { 303 | var $log = { 304 | log: function() { $log.log.logs.push(concat([], arguments, 0)); }, 305 | warn: function() { $log.warn.logs.push(concat([], arguments, 0)); }, 306 | info: function() { $log.info.logs.push(concat([], arguments, 0)); }, 307 | error: function() { $log.error.logs.push(concat([], arguments, 0)); } 308 | }; 309 | 310 | /** 311 | * @ngdoc method 312 | * @name ngMock.$log#reset 313 | * @methodOf ngMock.$log 314 | * 315 | * @description 316 | * Reset all of the logging arrays to empty. 317 | */ 318 | $log.reset = function () { 319 | /** 320 | * @ngdoc property 321 | * @name ngMock.$log#log.logs 322 | * @propertyOf ngMock.$log 323 | * 324 | * @description 325 | * Array of messages logged using {@link ngMock.$log#log}. 326 | * 327 | * @example 328 | *
 329 |        * $log.log('Some Log');
 330 |        * var first = $log.log.logs.unshift();
 331 |        * 
332 | */ 333 | $log.log.logs = []; 334 | /** 335 | * @ngdoc property 336 | * @name ngMock.$log#warn.logs 337 | * @propertyOf ngMock.$log 338 | * 339 | * @description 340 | * Array of messages logged using {@link ngMock.$log#warn}. 341 | * 342 | * @example 343 | *
 344 |        * $log.warn('Some Warning');
 345 |        * var first = $log.warn.logs.unshift();
 346 |        * 
347 | */ 348 | $log.warn.logs = []; 349 | /** 350 | * @ngdoc property 351 | * @name ngMock.$log#info.logs 352 | * @propertyOf ngMock.$log 353 | * 354 | * @description 355 | * Array of messages logged using {@link ngMock.$log#info}. 356 | * 357 | * @example 358 | *
 359 |        * $log.info('Some Info');
 360 |        * var first = $log.info.logs.unshift();
 361 |        * 
362 | */ 363 | $log.info.logs = []; 364 | /** 365 | * @ngdoc property 366 | * @name ngMock.$log#error.logs 367 | * @propertyOf ngMock.$log 368 | * 369 | * @description 370 | * Array of messages logged using {@link ngMock.$log#error}. 371 | * 372 | * @example 373 | *
 374 |        * $log.log('Some Error');
 375 |        * var first = $log.error.logs.unshift();
 376 |        * 
377 | */ 378 | $log.error.logs = []; 379 | }; 380 | 381 | /** 382 | * @ngdoc method 383 | * @name ngMock.$log#assertEmpty 384 | * @methodOf ngMock.$log 385 | * 386 | * @description 387 | * Assert that the all of the logging methods have no logged messages. If messages present, an exception is thrown. 388 | */ 389 | $log.assertEmpty = function() { 390 | var errors = []; 391 | angular.forEach(['error', 'warn', 'info', 'log'], function(logLevel) { 392 | angular.forEach($log[logLevel].logs, function(log) { 393 | angular.forEach(log, function (logItem) { 394 | errors.push('MOCK $log (' + logLevel + '): ' + String(logItem) + '\n' + (logItem.stack || '')); 395 | }); 396 | }); 397 | }); 398 | if (errors.length) { 399 | errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or an expected " + 400 | "log message was not checked and removed:"); 401 | errors.push(''); 402 | throw new Error(errors.join('\n---------\n')); 403 | } 404 | }; 405 | 406 | $log.reset(); 407 | return $log; 408 | }; 409 | }; 410 | 411 | 412 | (function() { 413 | var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/; 414 | 415 | function jsonStringToDate(string){ 416 | var match; 417 | if (match = string.match(R_ISO8061_STR)) { 418 | var date = new Date(0), 419 | tzHour = 0, 420 | tzMin = 0; 421 | if (match[9]) { 422 | tzHour = int(match[9] + match[10]); 423 | tzMin = int(match[9] + match[11]); 424 | } 425 | date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3])); 426 | date.setUTCHours(int(match[4]||0) - tzHour, int(match[5]||0) - tzMin, int(match[6]||0), int(match[7]||0)); 427 | return date; 428 | } 429 | return string; 430 | } 431 | 432 | function int(str) { 433 | return parseInt(str, 10); 434 | } 435 | 436 | function padNumber(num, digits, trim) { 437 | var neg = ''; 438 | if (num < 0) { 439 | neg = '-'; 440 | num = -num; 441 | } 442 | num = '' + num; 443 | while(num.length < digits) num = '0' + num; 444 | if (trim) 445 | num = num.substr(num.length - digits); 446 | return neg + num; 447 | } 448 | 449 | 450 | /** 451 | * @ngdoc object 452 | * @name angular.mock.TzDate 453 | * @description 454 | * 455 | * *NOTE*: this is not an injectable instance, just a globally available mock class of `Date`. 456 | * 457 | * Mock of the Date type which has its timezone specified via constructor arg. 458 | * 459 | * The main purpose is to create Date-like instances with timezone fixed to the specified timezone 460 | * offset, so that we can test code that depends on local timezone settings without dependency on 461 | * the time zone settings of the machine where the code is running. 462 | * 463 | * @param {number} offset Offset of the *desired* timezone in hours (fractions will be honored) 464 | * @param {(number|string)} timestamp Timestamp representing the desired time in *UTC* 465 | * 466 | * @example 467 | * !!!! WARNING !!!!! 468 | * This is not a complete Date object so only methods that were implemented can be called safely. 469 | * To make matters worse, TzDate instances inherit stuff from Date via a prototype. 470 | * 471 | * We do our best to intercept calls to "unimplemented" methods, but since the list of methods is 472 | * incomplete we might be missing some non-standard methods. This can result in errors like: 473 | * "Date.prototype.foo called on incompatible Object". 474 | * 475 | *
 476 |    * var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z');
 477 |    * newYearInBratislava.getTimezoneOffset() => -60;
 478 |    * newYearInBratislava.getFullYear() => 2010;
 479 |    * newYearInBratislava.getMonth() => 0;
 480 |    * newYearInBratislava.getDate() => 1;
 481 |    * newYearInBratislava.getHours() => 0;
 482 |    * newYearInBratislava.getMinutes() => 0;
 483 |    * 
484 | * 485 | */ 486 | angular.mock.TzDate = function (offset, timestamp) { 487 | var self = new Date(0); 488 | if (angular.isString(timestamp)) { 489 | var tsStr = timestamp; 490 | 491 | self.origDate = jsonStringToDate(timestamp); 492 | 493 | timestamp = self.origDate.getTime(); 494 | if (isNaN(timestamp)) 495 | throw { 496 | name: "Illegal Argument", 497 | message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string" 498 | }; 499 | } else { 500 | self.origDate = new Date(timestamp); 501 | } 502 | 503 | var localOffset = new Date(timestamp).getTimezoneOffset(); 504 | self.offsetDiff = localOffset*60*1000 - offset*1000*60*60; 505 | self.date = new Date(timestamp + self.offsetDiff); 506 | 507 | self.getTime = function() { 508 | return self.date.getTime() - self.offsetDiff; 509 | }; 510 | 511 | self.toLocaleDateString = function() { 512 | return self.date.toLocaleDateString(); 513 | }; 514 | 515 | self.getFullYear = function() { 516 | return self.date.getFullYear(); 517 | }; 518 | 519 | self.getMonth = function() { 520 | return self.date.getMonth(); 521 | }; 522 | 523 | self.getDate = function() { 524 | return self.date.getDate(); 525 | }; 526 | 527 | self.getHours = function() { 528 | return self.date.getHours(); 529 | }; 530 | 531 | self.getMinutes = function() { 532 | return self.date.getMinutes(); 533 | }; 534 | 535 | self.getSeconds = function() { 536 | return self.date.getSeconds(); 537 | }; 538 | 539 | self.getTimezoneOffset = function() { 540 | return offset * 60; 541 | }; 542 | 543 | self.getUTCFullYear = function() { 544 | return self.origDate.getUTCFullYear(); 545 | }; 546 | 547 | self.getUTCMonth = function() { 548 | return self.origDate.getUTCMonth(); 549 | }; 550 | 551 | self.getUTCDate = function() { 552 | return self.origDate.getUTCDate(); 553 | }; 554 | 555 | self.getUTCHours = function() { 556 | return self.origDate.getUTCHours(); 557 | }; 558 | 559 | self.getUTCMinutes = function() { 560 | return self.origDate.getUTCMinutes(); 561 | }; 562 | 563 | self.getUTCSeconds = function() { 564 | return self.origDate.getUTCSeconds(); 565 | }; 566 | 567 | self.getUTCMilliseconds = function() { 568 | return self.origDate.getUTCMilliseconds(); 569 | }; 570 | 571 | self.getDay = function() { 572 | return self.date.getDay(); 573 | }; 574 | 575 | // provide this method only on browsers that already have it 576 | if (self.toISOString) { 577 | self.toISOString = function() { 578 | return padNumber(self.origDate.getUTCFullYear(), 4) + '-' + 579 | padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' + 580 | padNumber(self.origDate.getUTCDate(), 2) + 'T' + 581 | padNumber(self.origDate.getUTCHours(), 2) + ':' + 582 | padNumber(self.origDate.getUTCMinutes(), 2) + ':' + 583 | padNumber(self.origDate.getUTCSeconds(), 2) + '.' + 584 | padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z' 585 | } 586 | } 587 | 588 | //hide all methods not implemented in this mock that the Date prototype exposes 589 | var unimplementedMethods = ['getMilliseconds', 'getUTCDay', 590 | 'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds', 591 | 'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear', 592 | 'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds', 593 | 'setYear', 'toDateString', 'toGMTString', 'toJSON', 'toLocaleFormat', 'toLocaleString', 594 | 'toLocaleTimeString', 'toSource', 'toString', 'toTimeString', 'toUTCString', 'valueOf']; 595 | 596 | angular.forEach(unimplementedMethods, function(methodName) { 597 | self[methodName] = function() { 598 | throw Error("Method '" + methodName + "' is not implemented in the TzDate mock"); 599 | }; 600 | }); 601 | 602 | return self; 603 | }; 604 | 605 | //make "tzDateInstance instanceof Date" return true 606 | angular.mock.TzDate.prototype = Date.prototype; 607 | })(); 608 | 609 | 610 | /** 611 | * @ngdoc function 612 | * @name angular.mock.dump 613 | * @description 614 | * 615 | * *NOTE*: this is not an injectable instance, just a globally available function. 616 | * 617 | * Method for serializing common angular objects (scope, elements, etc..) into strings, useful for debugging. 618 | * 619 | * This method is also available on window, where it can be used to display objects on debug console. 620 | * 621 | * @param {*} object - any object to turn into string. 622 | * @return {string} a serialized string of the argument 623 | */ 624 | angular.mock.dump = function(object) { 625 | return serialize(object); 626 | 627 | function serialize(object) { 628 | var out; 629 | 630 | if (angular.isElement(object)) { 631 | object = angular.element(object); 632 | out = angular.element('
'); 633 | angular.forEach(object, function(element) { 634 | out.append(angular.element(element).clone()); 635 | }); 636 | out = out.html(); 637 | } else if (angular.isArray(object)) { 638 | out = []; 639 | angular.forEach(object, function(o) { 640 | out.push(serialize(o)); 641 | }); 642 | out = '[ ' + out.join(', ') + ' ]'; 643 | } else if (angular.isObject(object)) { 644 | if (angular.isFunction(object.$eval) && angular.isFunction(object.$apply)) { 645 | out = serializeScope(object); 646 | } else if (object instanceof Error) { 647 | out = object.stack || ('' + object.name + ': ' + object.message); 648 | } else { 649 | out = angular.toJson(object, true); 650 | } 651 | } else { 652 | out = String(object); 653 | } 654 | 655 | return out; 656 | } 657 | 658 | function serializeScope(scope, offset) { 659 | offset = offset || ' '; 660 | var log = [offset + 'Scope(' + scope.$id + '): {']; 661 | for ( var key in scope ) { 662 | if (scope.hasOwnProperty(key) && !key.match(/^(\$|this)/)) { 663 | log.push(' ' + key + ': ' + angular.toJson(scope[key])); 664 | } 665 | } 666 | var child = scope.$$childHead; 667 | while(child) { 668 | log.push(serializeScope(child, offset + ' ')); 669 | child = child.$$nextSibling; 670 | } 671 | log.push('}'); 672 | return log.join('\n' + offset); 673 | } 674 | }; 675 | 676 | /** 677 | * @ngdoc object 678 | * @name ngMock.$httpBackend 679 | * @description 680 | * Fake HTTP backend implementation suitable for unit testing applications that use the 681 | * {@link ng.$http $http service}. 682 | * 683 | * *Note*: For fake HTTP backend implementation suitable for end-to-end testing or backend-less 684 | * development please see {@link ngMockE2E.$httpBackend e2e $httpBackend mock}. 685 | * 686 | * During unit testing, we want our unit tests to run quickly and have no external dependencies so 687 | * we don’t want to send {@link https://developer.mozilla.org/en/xmlhttprequest XHR} or 688 | * {@link http://en.wikipedia.org/wiki/JSONP JSONP} requests to a real server. All we really need is 689 | * to verify whether a certain request has been sent or not, or alternatively just let the 690 | * application make requests, respond with pre-trained responses and assert that the end result is 691 | * what we expect it to be. 692 | * 693 | * This mock implementation can be used to respond with static or dynamic responses via the 694 | * `expect` and `when` apis and their shortcuts (`expectGET`, `whenPOST`, etc). 695 | * 696 | * When an Angular application needs some data from a server, it calls the $http service, which 697 | * sends the request to a real server using $httpBackend service. With dependency injection, it is 698 | * easy to inject $httpBackend mock (which has the same API as $httpBackend) and use it to verify 699 | * the requests and respond with some testing data without sending a request to real server. 700 | * 701 | * There are two ways to specify what test data should be returned as http responses by the mock 702 | * backend when the code under test makes http requests: 703 | * 704 | * - `$httpBackend.expect` - specifies a request expectation 705 | * - `$httpBackend.when` - specifies a backend definition 706 | * 707 | * 708 | * # Request Expectations vs Backend Definitions 709 | * 710 | * Request expectations provide a way to make assertions about requests made by the application and 711 | * to define responses for those requests. The test will fail if the expected requests are not made 712 | * or they are made in the wrong order. 713 | * 714 | * Backend definitions allow you to define a fake backend for your application which doesn't assert 715 | * if a particular request was made or not, it just returns a trained response if a request is made. 716 | * The test will pass whether or not the request gets made during testing. 717 | * 718 | * 719 | * 720 | * 721 | * 722 | * 723 | * 724 | * 725 | * 726 | * 727 | * 728 | * 729 | * 730 | * 731 | * 732 | * 733 | * 734 | * 735 | * 736 | * 737 | * 738 | * 739 | * 740 | * 741 | * 742 | * 743 | * 744 | * 745 | * 746 | * 747 | * 748 | * 749 | * 750 | * 751 | *
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
752 | * 753 | * In cases where both backend definitions and request expectations are specified during unit 754 | * testing, the request expectations are evaluated first. 755 | * 756 | * If a request expectation has no response specified, the algorithm will search your backend 757 | * definitions for an appropriate response. 758 | * 759 | * If a request didn't match any expectation or if the expectation doesn't have the response 760 | * defined, the backend definitions are evaluated in sequential order to see if any of them match 761 | * the request. The response from the first matched definition is returned. 762 | * 763 | * 764 | * # Flushing HTTP requests 765 | * 766 | * The $httpBackend used in production, always responds to requests with responses asynchronously. 767 | * If we preserved this behavior in unit testing, we'd have to create async unit tests, which are 768 | * hard to write, follow and maintain. At the same time the testing mock, can't respond 769 | * synchronously because that would change the execution of the code under test. For this reason the 770 | * mock $httpBackend has a `flush()` method, which allows the test to explicitly flush pending 771 | * requests and thus preserving the async api of the backend, while allowing the test to execute 772 | * synchronously. 773 | * 774 | * 775 | * # Unit testing with mock $httpBackend 776 | * 777 | *
 778 |    // controller
 779 |    function MyController($scope, $http) {
 780 |      $http.get('/auth.py').success(function(data) {
 781 |        $scope.user = data;
 782 |      });
 783 | 
 784 |      this.saveMessage = function(message) {
 785 |        $scope.status = 'Saving...';
 786 |        $http.post('/add-msg.py', message).success(function(response) {
 787 |          $scope.status = '';
 788 |        }).error(function() {
 789 |          $scope.status = 'ERROR!';
 790 |        });
 791 |      };
 792 |    }
 793 | 
 794 |    // testing controller
 795 |    var $httpBackend;
 796 | 
 797 |    beforeEach(inject(function($injector) {
 798 |      $httpBackend = $injector.get('$httpBackend');
 799 | 
 800 |      // backend definition common for all tests
 801 |      $httpBackend.when('GET', '/auth.py').respond({userId: 'userX'}, {'A-Token': 'xxx'});
 802 |    }));
 803 | 
 804 | 
 805 |    afterEach(function() {
 806 |      $httpBackend.verifyNoOutstandingExpectation();
 807 |      $httpBackend.verifyNoOutstandingRequest();
 808 |    });
 809 | 
 810 | 
 811 |    it('should fetch authentication token', function() {
 812 |      $httpBackend.expectGET('/auth.py');
 813 |      var controller = scope.$new(MyController);
 814 |      $httpBackend.flush();
 815 |    });
 816 | 
 817 | 
 818 |    it('should send msg to server', function() {
 819 |      // now you don’t care about the authentication, but
 820 |      // the controller will still send the request and
 821 |      // $httpBackend will respond without you having to
 822 |      // specify the expectation and response for this request
 823 |      $httpBackend.expectPOST('/add-msg.py', 'message content').respond(201, '');
 824 | 
 825 |      var controller = scope.$new(MyController);
 826 |      $httpBackend.flush();
 827 |      controller.saveMessage('message content');
 828 |      expect(controller.status).toBe('Saving...');
 829 |      $httpBackend.flush();
 830 |      expect(controller.status).toBe('');
 831 |    });
 832 | 
 833 | 
 834 |    it('should send auth header', function() {
 835 |      $httpBackend.expectPOST('/add-msg.py', undefined, function(headers) {
 836 |        // check if the header was send, if it wasn't the expectation won't
 837 |        // match the request and the test will fail
 838 |        return headers['Authorization'] == 'xxx';
 839 |      }).respond(201, '');
 840 | 
 841 |      var controller = scope.$new(MyController);
 842 |      controller.saveMessage('whatever');
 843 |      $httpBackend.flush();
 844 |    });
 845 |    
846 | */ 847 | angular.mock.$HttpBackendProvider = function() { 848 | this.$get = [createHttpBackendMock]; 849 | }; 850 | 851 | /** 852 | * General factory function for $httpBackend mock. 853 | * Returns instance for unit testing (when no arguments specified): 854 | * - passing through is disabled 855 | * - auto flushing is disabled 856 | * 857 | * Returns instance for e2e testing (when `$delegate` and `$browser` specified): 858 | * - passing through (delegating request to real backend) is enabled 859 | * - auto flushing is enabled 860 | * 861 | * @param {Object=} $delegate Real $httpBackend instance (allow passing through if specified) 862 | * @param {Object=} $browser Auto-flushing enabled if specified 863 | * @return {Object} Instance of $httpBackend mock 864 | */ 865 | function createHttpBackendMock($delegate, $browser) { 866 | var definitions = [], 867 | expectations = [], 868 | responses = [], 869 | responsesPush = angular.bind(responses, responses.push); 870 | 871 | function createResponse(status, data, headers) { 872 | if (angular.isFunction(status)) return status; 873 | 874 | return function() { 875 | return angular.isNumber(status) 876 | ? [status, data, headers] 877 | : [200, status, data]; 878 | }; 879 | } 880 | 881 | // TODO(vojta): change params to: method, url, data, headers, callback 882 | function $httpBackend(method, url, data, callback, headers) { 883 | var xhr = new MockXhr(), 884 | expectation = expectations[0], 885 | wasExpected = false; 886 | 887 | function prettyPrint(data) { 888 | return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp) 889 | ? data 890 | : angular.toJson(data); 891 | } 892 | 893 | if (expectation && expectation.match(method, url)) { 894 | if (!expectation.matchData(data)) 895 | throw Error('Expected ' + expectation + ' with different data\n' + 896 | 'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data); 897 | 898 | if (!expectation.matchHeaders(headers)) 899 | throw Error('Expected ' + expectation + ' with different headers\n' + 900 | 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' + 901 | prettyPrint(headers)); 902 | 903 | expectations.shift(); 904 | 905 | if (expectation.response) { 906 | responses.push(function() { 907 | var response = expectation.response(method, url, data, headers); 908 | xhr.$$respHeaders = response[2]; 909 | callback(response[0], response[1], xhr.getAllResponseHeaders()); 910 | }); 911 | return; 912 | } 913 | wasExpected = true; 914 | } 915 | 916 | var i = -1, definition; 917 | while ((definition = definitions[++i])) { 918 | if (definition.match(method, url, data, headers || {})) { 919 | if (definition.response) { 920 | // if $browser specified, we do auto flush all requests 921 | ($browser ? $browser.defer : responsesPush)(function() { 922 | var response = definition.response(method, url, data, headers); 923 | xhr.$$respHeaders = response[2]; 924 | callback(response[0], response[1], xhr.getAllResponseHeaders()); 925 | }); 926 | } else if (definition.passThrough) { 927 | $delegate(method, url, data, callback, headers); 928 | } else throw Error('No response defined !'); 929 | return; 930 | } 931 | } 932 | throw wasExpected ? 933 | Error('No response defined !') : 934 | Error('Unexpected request: ' + method + ' ' + url + '\n' + 935 | (expectation ? 'Expected ' + expectation : 'No more request expected')); 936 | } 937 | 938 | /** 939 | * @ngdoc method 940 | * @name ngMock.$httpBackend#when 941 | * @methodOf ngMock.$httpBackend 942 | * @description 943 | * Creates a new backend definition. 944 | * 945 | * @param {string} method HTTP method. 946 | * @param {string|RegExp} url HTTP url. 947 | * @param {(string|RegExp)=} data HTTP request body. 948 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header 949 | * object and returns true if the headers match the current definition. 950 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 951 | * request is handled. 952 | * 953 | * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` 954 | * – The respond method takes a set of static data to be returned or a function that can return 955 | * an array containing response status (number), response data (string) and response headers 956 | * (Object). 957 | */ 958 | $httpBackend.when = function(method, url, data, headers) { 959 | var definition = new MockHttpExpectation(method, url, data, headers), 960 | chain = { 961 | respond: function(status, data, headers) { 962 | definition.response = createResponse(status, data, headers); 963 | } 964 | }; 965 | 966 | if ($browser) { 967 | chain.passThrough = function() { 968 | definition.passThrough = true; 969 | }; 970 | } 971 | 972 | definitions.push(definition); 973 | return chain; 974 | }; 975 | 976 | /** 977 | * @ngdoc method 978 | * @name ngMock.$httpBackend#whenGET 979 | * @methodOf ngMock.$httpBackend 980 | * @description 981 | * Creates a new backend definition for GET requests. For more info see `when()`. 982 | * 983 | * @param {string|RegExp} url HTTP url. 984 | * @param {(Object|function(Object))=} headers HTTP headers. 985 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 986 | * request is handled. 987 | */ 988 | 989 | /** 990 | * @ngdoc method 991 | * @name ngMock.$httpBackend#whenHEAD 992 | * @methodOf ngMock.$httpBackend 993 | * @description 994 | * Creates a new backend definition for HEAD requests. For more info see `when()`. 995 | * 996 | * @param {string|RegExp} url HTTP url. 997 | * @param {(Object|function(Object))=} headers HTTP headers. 998 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 999 | * request is handled. 1000 | */ 1001 | 1002 | /** 1003 | * @ngdoc method 1004 | * @name ngMock.$httpBackend#whenDELETE 1005 | * @methodOf ngMock.$httpBackend 1006 | * @description 1007 | * Creates a new backend definition for DELETE requests. For more info see `when()`. 1008 | * 1009 | * @param {string|RegExp} url HTTP url. 1010 | * @param {(Object|function(Object))=} headers HTTP headers. 1011 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1012 | * request is handled. 1013 | */ 1014 | 1015 | /** 1016 | * @ngdoc method 1017 | * @name ngMock.$httpBackend#whenPOST 1018 | * @methodOf ngMock.$httpBackend 1019 | * @description 1020 | * Creates a new backend definition for POST requests. For more info see `when()`. 1021 | * 1022 | * @param {string|RegExp} url HTTP url. 1023 | * @param {(string|RegExp)=} data HTTP request body. 1024 | * @param {(Object|function(Object))=} headers HTTP headers. 1025 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1026 | * request is handled. 1027 | */ 1028 | 1029 | /** 1030 | * @ngdoc method 1031 | * @name ngMock.$httpBackend#whenPUT 1032 | * @methodOf ngMock.$httpBackend 1033 | * @description 1034 | * Creates a new backend definition for PUT requests. For more info see `when()`. 1035 | * 1036 | * @param {string|RegExp} url HTTP url. 1037 | * @param {(string|RegExp)=} data HTTP request body. 1038 | * @param {(Object|function(Object))=} headers HTTP headers. 1039 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1040 | * request is handled. 1041 | */ 1042 | 1043 | /** 1044 | * @ngdoc method 1045 | * @name ngMock.$httpBackend#whenJSONP 1046 | * @methodOf ngMock.$httpBackend 1047 | * @description 1048 | * Creates a new backend definition for JSONP requests. For more info see `when()`. 1049 | * 1050 | * @param {string|RegExp} url HTTP url. 1051 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1052 | * request is handled. 1053 | */ 1054 | createShortMethods('when'); 1055 | 1056 | 1057 | /** 1058 | * @ngdoc method 1059 | * @name ngMock.$httpBackend#expect 1060 | * @methodOf ngMock.$httpBackend 1061 | * @description 1062 | * Creates a new request expectation. 1063 | * 1064 | * @param {string} method HTTP method. 1065 | * @param {string|RegExp} url HTTP url. 1066 | * @param {(string|RegExp)=} data HTTP request body. 1067 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header 1068 | * object and returns true if the headers match the current expectation. 1069 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1070 | * request is handled. 1071 | * 1072 | * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` 1073 | * – The respond method takes a set of static data to be returned or a function that can return 1074 | * an array containing response status (number), response data (string) and response headers 1075 | * (Object). 1076 | */ 1077 | $httpBackend.expect = function(method, url, data, headers) { 1078 | var expectation = new MockHttpExpectation(method, url, data, headers); 1079 | expectations.push(expectation); 1080 | return { 1081 | respond: function(status, data, headers) { 1082 | expectation.response = createResponse(status, data, headers); 1083 | } 1084 | }; 1085 | }; 1086 | 1087 | 1088 | /** 1089 | * @ngdoc method 1090 | * @name ngMock.$httpBackend#expectGET 1091 | * @methodOf ngMock.$httpBackend 1092 | * @description 1093 | * Creates a new request expectation for GET requests. For more info see `expect()`. 1094 | * 1095 | * @param {string|RegExp} url HTTP url. 1096 | * @param {Object=} headers HTTP headers. 1097 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1098 | * request is handled. See #expect for more info. 1099 | */ 1100 | 1101 | /** 1102 | * @ngdoc method 1103 | * @name ngMock.$httpBackend#expectHEAD 1104 | * @methodOf ngMock.$httpBackend 1105 | * @description 1106 | * Creates a new request expectation for HEAD requests. For more info see `expect()`. 1107 | * 1108 | * @param {string|RegExp} url HTTP url. 1109 | * @param {Object=} headers HTTP headers. 1110 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1111 | * request is handled. 1112 | */ 1113 | 1114 | /** 1115 | * @ngdoc method 1116 | * @name ngMock.$httpBackend#expectDELETE 1117 | * @methodOf ngMock.$httpBackend 1118 | * @description 1119 | * Creates a new request expectation for DELETE requests. For more info see `expect()`. 1120 | * 1121 | * @param {string|RegExp} url HTTP url. 1122 | * @param {Object=} headers HTTP headers. 1123 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1124 | * request is handled. 1125 | */ 1126 | 1127 | /** 1128 | * @ngdoc method 1129 | * @name ngMock.$httpBackend#expectPOST 1130 | * @methodOf ngMock.$httpBackend 1131 | * @description 1132 | * Creates a new request expectation for POST requests. For more info see `expect()`. 1133 | * 1134 | * @param {string|RegExp} url HTTP url. 1135 | * @param {(string|RegExp)=} data HTTP request body. 1136 | * @param {Object=} headers HTTP headers. 1137 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1138 | * request is handled. 1139 | */ 1140 | 1141 | /** 1142 | * @ngdoc method 1143 | * @name ngMock.$httpBackend#expectPUT 1144 | * @methodOf ngMock.$httpBackend 1145 | * @description 1146 | * Creates a new request expectation for PUT requests. For more info see `expect()`. 1147 | * 1148 | * @param {string|RegExp} url HTTP url. 1149 | * @param {(string|RegExp)=} data HTTP request body. 1150 | * @param {Object=} headers HTTP headers. 1151 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1152 | * request is handled. 1153 | */ 1154 | 1155 | /** 1156 | * @ngdoc method 1157 | * @name ngMock.$httpBackend#expectPATCH 1158 | * @methodOf ngMock.$httpBackend 1159 | * @description 1160 | * Creates a new request expectation for PATCH requests. For more info see `expect()`. 1161 | * 1162 | * @param {string|RegExp} url HTTP url. 1163 | * @param {(string|RegExp)=} data HTTP request body. 1164 | * @param {Object=} headers HTTP headers. 1165 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1166 | * request is handled. 1167 | */ 1168 | 1169 | /** 1170 | * @ngdoc method 1171 | * @name ngMock.$httpBackend#expectJSONP 1172 | * @methodOf ngMock.$httpBackend 1173 | * @description 1174 | * Creates a new request expectation for JSONP requests. For more info see `expect()`. 1175 | * 1176 | * @param {string|RegExp} url HTTP url. 1177 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1178 | * request is handled. 1179 | */ 1180 | createShortMethods('expect'); 1181 | 1182 | 1183 | /** 1184 | * @ngdoc method 1185 | * @name ngMock.$httpBackend#flush 1186 | * @methodOf ngMock.$httpBackend 1187 | * @description 1188 | * Flushes all pending requests using the trained responses. 1189 | * 1190 | * @param {number=} count Number of responses to flush (in the order they arrived). If undefined, 1191 | * all pending requests will be flushed. If there are no pending requests when the flush method 1192 | * is called an exception is thrown (as this typically a sign of programming error). 1193 | */ 1194 | $httpBackend.flush = function(count) { 1195 | if (!responses.length) throw Error('No pending request to flush !'); 1196 | 1197 | if (angular.isDefined(count)) { 1198 | while (count--) { 1199 | if (!responses.length) throw Error('No more pending request to flush !'); 1200 | responses.shift()(); 1201 | } 1202 | } else { 1203 | while (responses.length) { 1204 | responses.shift()(); 1205 | } 1206 | } 1207 | $httpBackend.verifyNoOutstandingExpectation(); 1208 | }; 1209 | 1210 | 1211 | /** 1212 | * @ngdoc method 1213 | * @name ngMock.$httpBackend#verifyNoOutstandingExpectation 1214 | * @methodOf ngMock.$httpBackend 1215 | * @description 1216 | * Verifies that all of the requests defined via the `expect` api were made. If any of the 1217 | * requests were not made, verifyNoOutstandingExpectation throws an exception. 1218 | * 1219 | * Typically, you would call this method following each test case that asserts requests using an 1220 | * "afterEach" clause. 1221 | * 1222 | *
1223 |    *   afterEach($httpBackend.verifyExpectations);
1224 |    * 
1225 | */ 1226 | $httpBackend.verifyNoOutstandingExpectation = function() { 1227 | if (expectations.length) { 1228 | throw Error('Unsatisfied requests: ' + expectations.join(', ')); 1229 | } 1230 | }; 1231 | 1232 | 1233 | /** 1234 | * @ngdoc method 1235 | * @name ngMock.$httpBackend#verifyNoOutstandingRequest 1236 | * @methodOf ngMock.$httpBackend 1237 | * @description 1238 | * Verifies that there are no outstanding requests that need to be flushed. 1239 | * 1240 | * Typically, you would call this method following each test case that asserts requests using an 1241 | * "afterEach" clause. 1242 | * 1243 | *
1244 |    *   afterEach($httpBackend.verifyNoOutstandingRequest);
1245 |    * 
1246 | */ 1247 | $httpBackend.verifyNoOutstandingRequest = function() { 1248 | if (responses.length) { 1249 | throw Error('Unflushed requests: ' + responses.length); 1250 | } 1251 | }; 1252 | 1253 | 1254 | /** 1255 | * @ngdoc method 1256 | * @name ngMock.$httpBackend#resetExpectations 1257 | * @methodOf ngMock.$httpBackend 1258 | * @description 1259 | * Resets all request expectations, but preserves all backend definitions. Typically, you would 1260 | * call resetExpectations during a multiple-phase test when you want to reuse the same instance of 1261 | * $httpBackend mock. 1262 | */ 1263 | $httpBackend.resetExpectations = function() { 1264 | expectations.length = 0; 1265 | responses.length = 0; 1266 | }; 1267 | 1268 | return $httpBackend; 1269 | 1270 | 1271 | function createShortMethods(prefix) { 1272 | angular.forEach(['GET', 'DELETE', 'JSONP'], function(method) { 1273 | $httpBackend[prefix + method] = function(url, headers) { 1274 | return $httpBackend[prefix](method, url, undefined, headers) 1275 | } 1276 | }); 1277 | 1278 | angular.forEach(['PUT', 'POST', 'PATCH'], function(method) { 1279 | $httpBackend[prefix + method] = function(url, data, headers) { 1280 | return $httpBackend[prefix](method, url, data, headers) 1281 | } 1282 | }); 1283 | } 1284 | } 1285 | 1286 | function MockHttpExpectation(method, url, data, headers) { 1287 | 1288 | this.data = data; 1289 | this.headers = headers; 1290 | 1291 | this.match = function(m, u, d, h) { 1292 | if (method != m) return false; 1293 | if (!this.matchUrl(u)) return false; 1294 | if (angular.isDefined(d) && !this.matchData(d)) return false; 1295 | if (angular.isDefined(h) && !this.matchHeaders(h)) return false; 1296 | return true; 1297 | }; 1298 | 1299 | this.matchUrl = function(u) { 1300 | if (!url) return true; 1301 | if (angular.isFunction(url.test)) return url.test(u); 1302 | return url == u; 1303 | }; 1304 | 1305 | this.matchHeaders = function(h) { 1306 | if (angular.isUndefined(headers)) return true; 1307 | if (angular.isFunction(headers)) return headers(h); 1308 | return angular.equals(headers, h); 1309 | }; 1310 | 1311 | this.matchData = function(d) { 1312 | if (angular.isUndefined(data)) return true; 1313 | if (data && angular.isFunction(data.test)) return data.test(d); 1314 | if (data && !angular.isString(data)) return angular.toJson(data) == d; 1315 | return data == d; 1316 | }; 1317 | 1318 | this.toString = function() { 1319 | return method + ' ' + url; 1320 | }; 1321 | } 1322 | 1323 | function MockXhr() { 1324 | 1325 | // hack for testing $http, $httpBackend 1326 | MockXhr.$$lastInstance = this; 1327 | 1328 | this.open = function(method, url, async) { 1329 | this.$$method = method; 1330 | this.$$url = url; 1331 | this.$$async = async; 1332 | this.$$reqHeaders = {}; 1333 | this.$$respHeaders = {}; 1334 | }; 1335 | 1336 | this.send = function(data) { 1337 | this.$$data = data; 1338 | }; 1339 | 1340 | this.setRequestHeader = function(key, value) { 1341 | this.$$reqHeaders[key] = value; 1342 | }; 1343 | 1344 | this.getResponseHeader = function(name) { 1345 | // the lookup must be case insensitive, that's why we try two quick lookups and full scan at last 1346 | var header = this.$$respHeaders[name]; 1347 | if (header) return header; 1348 | 1349 | name = angular.lowercase(name); 1350 | header = this.$$respHeaders[name]; 1351 | if (header) return header; 1352 | 1353 | header = undefined; 1354 | angular.forEach(this.$$respHeaders, function(headerVal, headerName) { 1355 | if (!header && angular.lowercase(headerName) == name) header = headerVal; 1356 | }); 1357 | return header; 1358 | }; 1359 | 1360 | this.getAllResponseHeaders = function() { 1361 | var lines = []; 1362 | 1363 | angular.forEach(this.$$respHeaders, function(value, key) { 1364 | lines.push(key + ': ' + value); 1365 | }); 1366 | return lines.join('\n'); 1367 | }; 1368 | 1369 | this.abort = angular.noop; 1370 | } 1371 | 1372 | 1373 | /** 1374 | * @ngdoc function 1375 | * @name ngMock.$timeout 1376 | * @description 1377 | * 1378 | * This service is just a simple decorator for {@link ng.$timeout $timeout} service 1379 | * that adds a "flush" method. 1380 | */ 1381 | 1382 | /** 1383 | * @ngdoc method 1384 | * @name ngMock.$timeout#flush 1385 | * @methodOf ngMock.$timeout 1386 | * @description 1387 | * 1388 | * Flushes the queue of pending tasks. 1389 | */ 1390 | 1391 | /** 1392 | * 1393 | */ 1394 | angular.mock.$RootElementProvider = function() { 1395 | this.$get = function() { 1396 | return angular.element('
'); 1397 | } 1398 | }; 1399 | 1400 | /** 1401 | * @ngdoc overview 1402 | * @name ngMock 1403 | * @description 1404 | * 1405 | * The `ngMock` is an angular module which is used with `ng` module and adds unit-test configuration as well as useful 1406 | * mocks to the {@link AUTO.$injector $injector}. 1407 | */ 1408 | angular.module('ngMock', ['ng']).provider({ 1409 | $browser: angular.mock.$BrowserProvider, 1410 | $exceptionHandler: angular.mock.$ExceptionHandlerProvider, 1411 | $log: angular.mock.$LogProvider, 1412 | $httpBackend: angular.mock.$HttpBackendProvider, 1413 | $rootElement: angular.mock.$RootElementProvider 1414 | }).config(function($provide) { 1415 | $provide.decorator('$timeout', function($delegate, $browser) { 1416 | $delegate.flush = function() { 1417 | $browser.defer.flush(); 1418 | }; 1419 | return $delegate; 1420 | }); 1421 | }); 1422 | 1423 | 1424 | /** 1425 | * @ngdoc overview 1426 | * @name ngMockE2E 1427 | * @description 1428 | * 1429 | * The `ngMockE2E` is an angular module which contains mocks suitable for end-to-end testing. 1430 | * Currently there is only one mock present in this module - 1431 | * the {@link ngMockE2E.$httpBackend e2e $httpBackend} mock. 1432 | */ 1433 | angular.module('ngMockE2E', ['ng']).config(function($provide) { 1434 | $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator); 1435 | }); 1436 | 1437 | /** 1438 | * @ngdoc object 1439 | * @name ngMockE2E.$httpBackend 1440 | * @description 1441 | * Fake HTTP backend implementation suitable for end-to-end testing or backend-less development of 1442 | * applications that use the {@link ng.$http $http service}. 1443 | * 1444 | * *Note*: For fake http backend implementation suitable for unit testing please see 1445 | * {@link ngMock.$httpBackend unit-testing $httpBackend mock}. 1446 | * 1447 | * This implementation can be used to respond with static or dynamic responses via the `when` api 1448 | * and its shortcuts (`whenGET`, `whenPOST`, etc) and optionally pass through requests to the 1449 | * real $httpBackend for specific requests (e.g. to interact with certain remote apis or to fetch 1450 | * templates from a webserver). 1451 | * 1452 | * As opposed to unit-testing, in an end-to-end testing scenario or in scenario when an application 1453 | * is being developed with the real backend api replaced with a mock, it is often desirable for 1454 | * certain category of requests to bypass the mock and issue a real http request (e.g. to fetch 1455 | * templates or static files from the webserver). To configure the backend with this behavior 1456 | * use the `passThrough` request handler of `when` instead of `respond`. 1457 | * 1458 | * Additionally, we don't want to manually have to flush mocked out requests like we do during unit 1459 | * testing. For this reason the e2e $httpBackend automatically flushes mocked out requests 1460 | * automatically, closely simulating the behavior of the XMLHttpRequest object. 1461 | * 1462 | * To setup the application to run with this http backend, you have to create a module that depends 1463 | * on the `ngMockE2E` and your application modules and defines the fake backend: 1464 | * 1465 | *
1466 |  *   myAppDev = angular.module('myAppDev', ['myApp', 'ngMockE2E']);
1467 |  *   myAppDev.run(function($httpBackend) {
1468 |  *     phones = [{name: 'phone1'}, {name: 'phone2'}];
1469 |  *
1470 |  *     // returns the current list of phones
1471 |  *     $httpBackend.whenGET('/phones').respond(phones);
1472 |  *
1473 |  *     // adds a new phone to the phones array
1474 |  *     $httpBackend.whenPOST('/phones').respond(function(method, url, data) {
1475 |  *       phones.push(angular.fromJSON(data));
1476 |  *     });
1477 |  *     $httpBackend.whenGET(/^\/templates\//).passThrough();
1478 |  *     //...
1479 |  *   });
1480 |  * 
1481 | * 1482 | * Afterwards, bootstrap your app with this new module. 1483 | */ 1484 | 1485 | /** 1486 | * @ngdoc method 1487 | * @name ngMockE2E.$httpBackend#when 1488 | * @methodOf ngMockE2E.$httpBackend 1489 | * @description 1490 | * Creates a new backend definition. 1491 | * 1492 | * @param {string} method HTTP method. 1493 | * @param {string|RegExp} url HTTP url. 1494 | * @param {(string|RegExp)=} data HTTP request body. 1495 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header 1496 | * object and returns true if the headers match the current definition. 1497 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1498 | * control how a matched request is handled. 1499 | * 1500 | * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` 1501 | * – The respond method takes a set of static data to be returned or a function that can return 1502 | * an array containing response status (number), response data (string) and response headers 1503 | * (Object). 1504 | * - passThrough – `{function()}` – Any request matching a backend definition with `passThrough` 1505 | * handler, will be pass through to the real backend (an XHR request will be made to the 1506 | * server. 1507 | */ 1508 | 1509 | /** 1510 | * @ngdoc method 1511 | * @name ngMockE2E.$httpBackend#whenGET 1512 | * @methodOf ngMockE2E.$httpBackend 1513 | * @description 1514 | * Creates a new backend definition for GET requests. For more info see `when()`. 1515 | * 1516 | * @param {string|RegExp} url HTTP url. 1517 | * @param {(Object|function(Object))=} headers HTTP headers. 1518 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1519 | * control how a matched request is handled. 1520 | */ 1521 | 1522 | /** 1523 | * @ngdoc method 1524 | * @name ngMockE2E.$httpBackend#whenHEAD 1525 | * @methodOf ngMockE2E.$httpBackend 1526 | * @description 1527 | * Creates a new backend definition for HEAD requests. For more info see `when()`. 1528 | * 1529 | * @param {string|RegExp} url HTTP url. 1530 | * @param {(Object|function(Object))=} headers HTTP headers. 1531 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1532 | * control how a matched request is handled. 1533 | */ 1534 | 1535 | /** 1536 | * @ngdoc method 1537 | * @name ngMockE2E.$httpBackend#whenDELETE 1538 | * @methodOf ngMockE2E.$httpBackend 1539 | * @description 1540 | * Creates a new backend definition for DELETE requests. For more info see `when()`. 1541 | * 1542 | * @param {string|RegExp} url HTTP url. 1543 | * @param {(Object|function(Object))=} headers HTTP headers. 1544 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1545 | * control how a matched request is handled. 1546 | */ 1547 | 1548 | /** 1549 | * @ngdoc method 1550 | * @name ngMockE2E.$httpBackend#whenPOST 1551 | * @methodOf ngMockE2E.$httpBackend 1552 | * @description 1553 | * Creates a new backend definition for POST requests. For more info see `when()`. 1554 | * 1555 | * @param {string|RegExp} url HTTP url. 1556 | * @param {(string|RegExp)=} data HTTP request body. 1557 | * @param {(Object|function(Object))=} headers HTTP headers. 1558 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1559 | * control how a matched request is handled. 1560 | */ 1561 | 1562 | /** 1563 | * @ngdoc method 1564 | * @name ngMockE2E.$httpBackend#whenPUT 1565 | * @methodOf ngMockE2E.$httpBackend 1566 | * @description 1567 | * Creates a new backend definition for PUT requests. For more info see `when()`. 1568 | * 1569 | * @param {string|RegExp} url HTTP url. 1570 | * @param {(string|RegExp)=} data HTTP request body. 1571 | * @param {(Object|function(Object))=} headers HTTP headers. 1572 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1573 | * control how a matched request is handled. 1574 | */ 1575 | 1576 | /** 1577 | * @ngdoc method 1578 | * @name ngMockE2E.$httpBackend#whenPATCH 1579 | * @methodOf ngMockE2E.$httpBackend 1580 | * @description 1581 | * Creates a new backend definition for PATCH requests. For more info see `when()`. 1582 | * 1583 | * @param {string|RegExp} url HTTP url. 1584 | * @param {(string|RegExp)=} data HTTP request body. 1585 | * @param {(Object|function(Object))=} headers HTTP headers. 1586 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1587 | * control how a matched request is handled. 1588 | */ 1589 | 1590 | /** 1591 | * @ngdoc method 1592 | * @name ngMockE2E.$httpBackend#whenJSONP 1593 | * @methodOf ngMockE2E.$httpBackend 1594 | * @description 1595 | * Creates a new backend definition for JSONP requests. For more info see `when()`. 1596 | * 1597 | * @param {string|RegExp} url HTTP url. 1598 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1599 | * control how a matched request is handled. 1600 | */ 1601 | angular.mock.e2e = {}; 1602 | angular.mock.e2e.$httpBackendDecorator = ['$delegate', '$browser', createHttpBackendMock]; 1603 | 1604 | 1605 | angular.mock.clearDataCache = function() { 1606 | var key, 1607 | cache = angular.element.cache; 1608 | 1609 | for(key in cache) { 1610 | if (cache.hasOwnProperty(key)) { 1611 | var handle = cache[key].handle; 1612 | 1613 | handle && angular.element(handle.elem).unbind(); 1614 | delete cache[key]; 1615 | } 1616 | } 1617 | }; 1618 | 1619 | 1620 | window.jstestdriver && (function(window) { 1621 | /** 1622 | * Global method to output any number of objects into JSTD console. Useful for debugging. 1623 | */ 1624 | window.dump = function() { 1625 | var args = []; 1626 | angular.forEach(arguments, function(arg) { 1627 | args.push(angular.mock.dump(arg)); 1628 | }); 1629 | jstestdriver.console.log.apply(jstestdriver.console, args); 1630 | if (window.console) { 1631 | window.console.log.apply(window.console, args); 1632 | } 1633 | }; 1634 | })(window); 1635 | 1636 | 1637 | window.jasmine && (function(window) { 1638 | 1639 | afterEach(function() { 1640 | var spec = getCurrentSpec(); 1641 | var injector = spec.$injector; 1642 | 1643 | spec.$injector = null; 1644 | spec.$modules = null; 1645 | 1646 | if (injector) { 1647 | injector.get('$rootElement').unbind(); 1648 | injector.get('$browser').pollFns.length = 0; 1649 | } 1650 | 1651 | angular.mock.clearDataCache(); 1652 | 1653 | // clean up jquery's fragment cache 1654 | angular.forEach(angular.element.fragments, function(val, key) { 1655 | delete angular.element.fragments[key]; 1656 | }); 1657 | 1658 | MockXhr.$$lastInstance = null; 1659 | 1660 | angular.forEach(angular.callbacks, function(val, key) { 1661 | delete angular.callbacks[key]; 1662 | }); 1663 | angular.callbacks.counter = 0; 1664 | }); 1665 | 1666 | function getCurrentSpec() { 1667 | return jasmine.getEnv().currentSpec; 1668 | } 1669 | 1670 | function isSpecRunning() { 1671 | var spec = getCurrentSpec(); 1672 | return spec && spec.queue.running; 1673 | } 1674 | 1675 | /** 1676 | * @ngdoc function 1677 | * @name angular.mock.module 1678 | * @description 1679 | * 1680 | * *NOTE*: This function is also published on window for easy access.
1681 | * *NOTE*: Only available with {@link http://pivotal.github.com/jasmine/ jasmine}. 1682 | * 1683 | * This function registers a module configuration code. It collects the configuration information 1684 | * which will be used when the injector is created by {@link angular.mock.inject inject}. 1685 | * 1686 | * See {@link angular.mock.inject inject} for usage example 1687 | * 1688 | * @param {...(string|Function)} fns any number of modules which are represented as string 1689 | * aliases or as anonymous module initialization functions. The modules are used to 1690 | * configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. 1691 | */ 1692 | window.module = angular.mock.module = function() { 1693 | var moduleFns = Array.prototype.slice.call(arguments, 0); 1694 | return isSpecRunning() ? workFn() : workFn; 1695 | ///////////////////// 1696 | function workFn() { 1697 | var spec = getCurrentSpec(); 1698 | if (spec.$injector) { 1699 | throw Error('Injector already created, can not register a module!'); 1700 | } else { 1701 | var modules = spec.$modules || (spec.$modules = []); 1702 | angular.forEach(moduleFns, function(module) { 1703 | modules.push(module); 1704 | }); 1705 | } 1706 | } 1707 | }; 1708 | 1709 | /** 1710 | * @ngdoc function 1711 | * @name angular.mock.inject 1712 | * @description 1713 | * 1714 | * *NOTE*: This function is also published on window for easy access.
1715 | * *NOTE*: Only available with {@link http://pivotal.github.com/jasmine/ jasmine}. 1716 | * 1717 | * The inject function wraps a function into an injectable function. The inject() creates new 1718 | * instance of {@link AUTO.$injector $injector} per test, which is then used for 1719 | * resolving references. 1720 | * 1721 | * See also {@link angular.mock.module module} 1722 | * 1723 | * Example of what a typical jasmine tests looks like with the inject method. 1724 | *
1725 |    *
1726 |    *   angular.module('myApplicationModule', [])
1727 |    *       .value('mode', 'app')
1728 |    *       .value('version', 'v1.0.1');
1729 |    *
1730 |    *
1731 |    *   describe('MyApp', function() {
1732 |    *
1733 |    *     // You need to load modules that you want to test,
1734 |    *     // it loads only the "ng" module by default.
1735 |    *     beforeEach(module('myApplicationModule'));
1736 |    *
1737 |    *
1738 |    *     // inject() is used to inject arguments of all given functions
1739 |    *     it('should provide a version', inject(function(mode, version) {
1740 |    *       expect(version).toEqual('v1.0.1');
1741 |    *       expect(mode).toEqual('app');
1742 |    *     }));
1743 |    *
1744 |    *
1745 |    *     // The inject and module method can also be used inside of the it or beforeEach
1746 |    *     it('should override a version and test the new version is injected', function() {
1747 |    *       // module() takes functions or strings (module aliases)
1748 |    *       module(function($provide) {
1749 |    *         $provide.value('version', 'overridden'); // override version here
1750 |    *       });
1751 |    *
1752 |    *       inject(function(version) {
1753 |    *         expect(version).toEqual('overridden');
1754 |    *       });
1755 |    *     ));
1756 |    *   });
1757 |    *
1758 |    * 
1759 | * 1760 | * @param {...Function} fns any number of functions which will be injected using the injector. 1761 | */ 1762 | window.inject = angular.mock.inject = function() { 1763 | var blockFns = Array.prototype.slice.call(arguments, 0); 1764 | var errorForStack = new Error('Declaration Location'); 1765 | return isSpecRunning() ? workFn() : workFn; 1766 | ///////////////////// 1767 | function workFn() { 1768 | var spec = getCurrentSpec(); 1769 | var modules = spec.$modules || []; 1770 | modules.unshift('ngMock'); 1771 | modules.unshift('ng'); 1772 | var injector = spec.$injector; 1773 | if (!injector) { 1774 | injector = spec.$injector = angular.injector(modules); 1775 | } 1776 | for(var i = 0, ii = blockFns.length; i < ii; i++) { 1777 | try { 1778 | injector.invoke(blockFns[i] || angular.noop, this); 1779 | } catch (e) { 1780 | if(e.stack && errorForStack) e.stack += '\n' + errorForStack.stack; 1781 | throw e; 1782 | } finally { 1783 | errorForStack = null; 1784 | } 1785 | } 1786 | } 1787 | }; 1788 | })(window); 1789 | -------------------------------------------------------------------------------- /test/lib/custom-mocks.js: -------------------------------------------------------------------------------- 1 | 2 | customMocks = { 3 | 4 | rootScope: { 5 | safeApply: jasmine.createSpy('safeApply').andCallFake(function(callback) { if (callback) {callback()}}) 6 | }, 7 | 8 | deferred: { 9 | resolve: jasmine.createSpy('resolve'), 10 | reject: jasmine.createSpy('reject'), 11 | promise: { 12 | then: function(callback) { 13 | callback(); 14 | } 15 | } 16 | }, 17 | 18 | q: { 19 | defer: function() { 20 | return customMocks.deferred; 21 | } 22 | }, 23 | 24 | Firebase: { 25 | new: (new Firebase()), 26 | mockSnapshot: {} 27 | } 28 | 29 | } 30 | 31 | 32 | function Firebase() {}; 33 | Firebase.prototype.child = jasmine.createSpy('child').andReturn(new Firebase()); 34 | Firebase.prototype.path = "/test"; 35 | Firebase.prototype.limit = jasmine.createSpy('limit').andReturn(new Firebase()); 36 | Firebase.prototype.name = jasmine.createSpy('name').andReturn('test'); 37 | Firebase.prototype.on = jasmine.createSpy('on').andCallFake(function(event, callback) { customMocks.Firebase['on_' + event] = callback}); 38 | Firebase.prototype.once = jasmine.createSpy('once').andCallFake(function(value, callback) { customMocks.Firebase['once'] = callback}); 39 | Firebase.prototype.push = jasmine.createSpy('push').andReturn(new Firebase()); 40 | Firebase.prototype.update = jasmine.createSpy('update').andCallFake(function(obj, callback) { callback() }); 41 | Firebase.prototype.remove = jasmine.createSpy('remove'); 42 | Firebase.prototype.set = jasmine.createSpy('set').andCallFake(function(bool, callback) { callback()}); 43 | Firebase.ServerValue = {TIMESTAMP: {".sv": "timestamp"}}; 44 | 45 | 46 | --------------------------------------------------------------------------------