├── .gitignore ├── .jshintrc ├── .travis.yml ├── API.md ├── GUIDE.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── dist ├── wild-angular.js └── wild-angular.min.js ├── index.js ├── package.json ├── src ├── WilddogArray.js ├── WilddogAuth.js ├── WilddogObject.js ├── lib │ └── polyfills.js ├── module.js ├── utils.js └── wilddog.js └── tests ├── automatic_karma.conf.js ├── browsers.json ├── fixtures └── data.json ├── lib ├── jasmineMatchers.js └── module.testutils.js ├── local_protractor.conf.js ├── manual └── auth.spec.js ├── manual_karma.conf.js ├── mocks └── mocks.firebase.js ├── protractor ├── chat │ ├── chat.css │ ├── chat.html │ ├── chat.js │ └── chat.spec.js ├── priority │ ├── priority.css │ ├── priority.html │ ├── priority.js │ └── priority.spec.js ├── tictactoe │ ├── tictactoe.css │ ├── tictactoe.html │ ├── tictactoe.js │ └── tictactoe.spec.js └── todo │ ├── todo.css │ ├── todo.html │ ├── todo.js │ └── todo.spec.js ├── sauce_karma.conf.js ├── sauce_protractor.conf.js ├── travis.sh └── unit ├── WilddogArray.spec.js ├── WilddogAuth.spec.js ├── WilddogObject.spec.js ├── utils.spec.js └── wilddog.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | tests/coverage/ 4 | .idea 5 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "angular", 4 | "Wilddog" 5 | ], 6 | "bitwise": true, 7 | "browser": true, 8 | "curly": true, 9 | "forin": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "node": true, 13 | "noempty": true, 14 | "nonbsp": true, 15 | "strict": true, 16 | "trailing": true, 17 | "undef": true, 18 | "unused": true 19 | } 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | sudo: false 5 | addons: 6 | sauce_connect: true 7 | before_install: 8 | - export DISPLAY=:99.0 9 | - sh -e /etc/init.d/xvfb start 10 | install: 11 | - git clone git://github.com/n1k0/casperjs.git ~/casperjs 12 | - export PATH=$PATH:~/casperjs/bin 13 | - npm install -g grunt-cli 14 | - npm install 15 | before_script: 16 | - grunt install 17 | - phantomjs --version 18 | script: 19 | - sh ./tests/travis.sh 20 | after_script: 21 | - cat ./tests/coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 22 | env: 23 | global: 24 | - secure: mGHp1rQI11OvbBQn3PnBT5kuyo26gFl8U+nNq0Ot4opgSBX9JaHqS8Dx63uALWWU9qjy08/Mn68t/sKhayH1+XrPDIenOy/XEkkSAG60qAAowD9dRo3WaIMSOcWWYDeqdZOAWZ3LiXvjLO4Swagz5ejz7UtY/ws4CcTi2n/fp7c= 25 | - secure: Eao+hPFWKrHb7qUGEzLg7zdTCE//gb3arf5UmI9Z3i+DydSu/AwExXuywJYUj4/JNm/z8zyJ3j1/mdTyyt9VVyrnQNnyGH1b2oCUHkrs1NLwh5Oe4YcqUYROzoEKdDInvmjVJnIfUEM07htGMGvsLsX4MW2tqVHvD2rOwkn8C9s= 26 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* global module */ 2 | module.exports = function(grunt) { 3 | 'use strict'; 4 | 5 | grunt.initConfig({ 6 | pkg: grunt.file.readJSON('package.json'), 7 | meta: { 8 | banner: '/*wilddog realtime*/\n' 9 | }, 10 | 11 | // merge files from src/ into angularfire.js 12 | concat: { 13 | app: { 14 | options: { banner: '<%= meta.banner %>' }, 15 | src: [ 16 | 'src/module.js', 17 | 'src/**/*.js' 18 | ], 19 | dest: 'dist/wild-angular.js' 20 | } 21 | }, 22 | 23 | // Run shell commands 24 | shell: { 25 | options: { 26 | stdout: true 27 | }, 28 | protractor_install: { 29 | command: 'node ./node_modules/protractor/bin/webdriver-manager update' 30 | }, 31 | npm_install: { 32 | command: 'npm install' 33 | }, 34 | bower_install: { 35 | command: 'bower install' 36 | } 37 | }, 38 | 39 | // Create local server 40 | connect: { 41 | testserver: { 42 | options: { 43 | hostname: 'localhost', 44 | port: 3030 45 | } 46 | } 47 | }, 48 | 49 | // Minify JavaScript 50 | uglify : { 51 | options: { 52 | preserveComments: 'some' 53 | }, 54 | app : { 55 | files : { 56 | 'dist/wild-angular.min.js' : ['dist/wild-angular.js'] 57 | } 58 | } 59 | }, 60 | 61 | // Lint JavaScript 62 | jshint : { 63 | options : { 64 | jshintrc: '.jshintrc', 65 | ignores: ['src/lib/polyfills.js'] 66 | }, 67 | all : ['src/**/*.js'] 68 | }, 69 | 70 | // Auto-run tasks on file changes 71 | watch : { 72 | scripts : { 73 | files : ['src/**/*.js', 'tests/unit/**/*.spec.js', 'tests/lib/**/*.js'], 74 | tasks : ['test:unit', 'notify:watch'], 75 | options : { 76 | interrupt : true, 77 | atBegin: true 78 | } 79 | } 80 | }, 81 | 82 | // Unit tests 83 | karma: { 84 | options: { 85 | configFile: 'tests/automatic_karma.conf.js' 86 | }, 87 | manual: { 88 | configFile: 'tests/manual_karma.conf.js' 89 | }, 90 | singlerun: {}, 91 | watch: { 92 | autowatch: true, 93 | singleRun: false 94 | }, 95 | saucelabs: { 96 | configFile: 'tests/sauce_karma.conf.js' 97 | } 98 | }, 99 | 100 | // End to end (e2e) tests 101 | protractor: { 102 | options: { 103 | configFile: "tests/local_protractor.conf.js" 104 | }, 105 | singlerun: {}, 106 | saucelabs: { 107 | options: { 108 | configFile: "tests/sauce_protractor.conf.js", 109 | args: { 110 | sauceUser: process.env.SAUCE_USERNAME, 111 | sauceKey: process.env.SAUCE_ACCESS_KEY 112 | } 113 | } 114 | } 115 | }, 116 | 117 | // Desktop notificaitons 118 | notify: { 119 | watch: { 120 | options: { 121 | title: 'Grunt Watch', 122 | message: 'Build Finished' 123 | } 124 | } 125 | } 126 | }); 127 | 128 | require('load-grunt-tasks')(grunt); 129 | 130 | // Installation 131 | grunt.registerTask('install', ['shell:protractor_install']); 132 | grunt.registerTask('update', ['shell:npm_install']); 133 | 134 | // Single run tests 135 | grunt.registerTask('test', ['test:unit', 'test:e2e']); 136 | 137 | // grunt.registerTask('test', ['test:unit']); 138 | grunt.registerTask('test:unit', ['karma:singlerun']); 139 | grunt.registerTask('test:e2e', ['concat', 'connect:testserver', 'protractor:singlerun']); 140 | grunt.registerTask('test:manual', ['karma:manual']); 141 | 142 | // Travis CI testing 143 | //grunt.registerTask('test:travis', ['build', 'test:unit', 'connect:testserver', 'protractor:saucelabs']); 144 | grunt.registerTask('test:travis', ['build', 'test:unit']); 145 | 146 | // Sauce tasks 147 | grunt.registerTask('sauce:unit', ['karma:saucelabs']); 148 | grunt.registerTask('sauce:e2e', ['concat', 'connect:testserver', 'protractor:saucelabs']); 149 | 150 | // Watch tests 151 | grunt.registerTask('test:watch', ['karma:watch']); 152 | grunt.registerTask('test:watch:unit', ['karma:watch']); 153 | 154 | // Build tasks 155 | grunt.registerTask('build', ['concat', 'jshint', 'uglify']); 156 | 157 | // Default task 158 | grunt.registerTask('default', ['build', 'test']); 159 | }; 160 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Wilddog 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # lib-js-wildangular 3 | 4 | 5 | [![Build Status](https://travis-ci.org/WildDogTeam/lib-js-wildangular.svg?branch=master)](https://travis-ci.org/WildDogTeam/lib-js-wildangular) 6 | [![Coverage Status](https://coveralls.io/repos/WildDogTeam/wild-angular/badge.svg?branch=master&service=github)](https://coveralls.io/github/WildDogTeam/wild-angular?branch=master) 7 | [![Version](https://badge.fury.io/gh/WildDogTeam%2Flib-js-wildangular.svg)](http://badge.fury.io/gh/WildDogTeam%2Flib-js-wildangular) 8 | 9 | wild-angular 是Wilddog对angularJS的官方支持库。[野狗实时后端云](http://www.wilddog.com/?utm_medium=web&utm_source=lib-js-wildangular) 是支持数据存储,读写,身份认证的后端服务。 10 | 11 | wild-angular 是对Wilddog客户端的补充,提供三个angular service 12 | * `$wilddogObject` - 同步Object 13 | * `$wilddogArray` - 同步Array 14 | * `$wilddogAuth` - 认证 15 | 16 | 17 | ## quickStart 18 | 19 | #### 引入依赖 20 | 21 | 22 | 在html中使用wild-angular: 23 | 24 | ```html 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ``` 34 | 使用npm: 35 | 36 | ```bash 37 | $ npm install wild-angular --save 38 | ``` 39 | 40 | 使用bower: 41 | ```bash 42 | $ bower install wild-angular --save 43 | ``` 44 | 45 | #### 注入lib-js-wildangular服务 46 | 47 | 在我们通过依赖注入使用wild-angular之前,我们需要注册一个`wilddog`模块 48 | 49 | ``` 50 | var app = angular.module("sampleApp",["wilddog"]); 51 | 52 | ``` 53 | 现在 `$wilddogObject`,`$wilddogArray`,`$wilddogAuth` Service 可以被注入到任何,controller,service 或factory。 54 | 55 | ``` 56 | app.controller("SampleCtrl",function($scope,$wilddogObject){ 57 | var config = { 58 | authDomain: ".wilddog.com", 59 | syncURL: "https://.wilddogio.com/" 60 | }; 61 | wilddog.initializeApp(config); 62 | var auth = wilddog.auth(); 63 | var sync = wilddog.sync(); 64 | 65 | //将数据下载到一个本地对象 66 | $scope.data = $wilddogObject(sync.ref()); 67 | 68 | //在这里打印数据会得到空值 69 | 70 | }); 71 | 72 | ``` 73 | 74 | 在上面的例子中,`$scope.data` 将会与服务器同步 。 75 | 76 | 77 | 78 | #### 三向数据对象绑定 79 | 80 | Angular被大家熟知的是它的双向数据绑定特性。Wilddog是一个轻量的实时数据库。wild-angular可以将两者完美的结合在一起,DOM,javascript和Wilddog数据库三者之间实时同步。DOM发生改变,相应的model发生改变,与这个model绑定的Wilddog数据节点也发生相应的改变。 81 | 82 | 如何设置三向数据绑定?我们使用前面介绍的`$wilddogObject`service来创建同步对象,然后调用`$bindTo()`来绑定到`$scope`的一个变量上。 83 | 84 | app.js 85 | 86 | ``` js 87 | var app = angular.module("sampleApp",['wilddog']); 88 | app.controller("SampleCtrl",function($scope,$wilddogObject){ 89 | var config = { 90 | authDomain: ".wilddog.com", 91 | syncURL: "https://.wilddogio.com/" 92 | }; 93 | wilddog.initializeApp(config); 94 | var auth = wilddog.auth(); 95 | var sync = wilddog.sync(); 96 | 97 | //将云端数据与本地变量同步 98 | var syncObject = $wilddogObject(sync.ref('data')); 99 | 100 | //将Wilddog绑定到$scope.data,当本地model发生变化,Wilddog数据库也同步变化。 101 | syncObject.$bindTo($scope,"data"); 102 | 103 | }) 104 | 105 | ``` 106 | index.html 107 | 108 | ``` html 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 |

You said: {{ data.text }}

121 | 122 | 123 | 124 | ``` 125 | 126 | #### 同步数组 127 | 128 | 三向数据绑定对于简单的key-value数据非常好用。但是,很多时候我们需要使用列表。这时我们需要使用`$wilddogArray`service 129 | 130 | 131 | 我们先看看只读的方式,从Wilddog数据库中把一个列表下载到`$scope`的一个对象中 132 | 133 | app.js 134 | ``` js 135 | 136 | var app = angular.module("sampleApp", ["wilddog"]); 137 | app.controller("SampleCtrl", function($scope, $wilddogArray) { 138 | var config = { 139 | authDomain: ".wilddog.com", 140 | syncURL: "https://.wilddogio.com" 141 | }; 142 | wilddog.initializeApp(config); 143 | var auth = wilddog.auth(); 144 | var sync = wilddog.sync(); 145 | 146 | // 创建一个同步数组 147 | $scope.messages = $wilddogArray(sync.ref('messages')); 148 | }); 149 | 150 | ``` 151 | 152 | index.html 153 | 154 | ``` html 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 |
    164 |
  • {{ message.text }}
  • 165 |
166 | 167 | 168 | 169 | 170 | ``` 171 | 172 | 上面的例子只能满足只读的需求,Wilddog数据库修改,页面会保持同步,而如果页面中的数据发生了修改,将不会通知到Wilddog数据库。 173 | 174 | 175 | 那么,wild-angular提供了一组方法来完成同步数组的需求:`$add()`,`$(save)`,`$(remove)` 176 | 177 | app.js 178 | 179 | ``` js 180 | 181 | var app = angular.module("sampleApp", ["wilddog"]); 182 | app.controller("SampleCtrl", function($scope, $wilddogArray) { 183 | var config = { 184 | syncURL: "https://.wilddogio.com" 185 | }; 186 | wilddog.initializeApp(config); 187 | var sync = wilddog.sync(); 188 | // 创建一个同步数组 189 | $scope.messages = $wilddogArray(sync.ref('messages')); 190 | // 把新数据添加到列表中 191 | // 这条数据会自动同步到wilddog数据库 192 | $scope.addMessage = function() { 193 | $scope.messages.$add({ 194 | text: $scope.newMessageText 195 | }); 196 | }; 197 | }); 198 | 199 | ``` 200 | 201 | index.html 202 | 203 | ``` html 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 |
    212 |
  • 213 | 214 | 215 | 216 | 217 |
  • 218 |
219 | 220 |
221 | 222 | 223 |
224 | 225 | 226 | 227 | 228 | ``` 229 | #### 终端用户认证 230 | 231 | Wilddog 提供了一列登录认证的方式,支持匿名,email,微博,微信,QQ登录 232 | 233 | 234 | wild-angular提供了一个service `$wilddogAuth`,封装了Wilddog提供的登录认证的方式。能够注入到任何 controller,service和factory。 235 | 236 | ``` 237 | app.controller("SampleCtrl", function($scope, $wilddogAuth) { 238 | var config = { 239 | authDomain: ".wilddog.com", 240 | }; 241 | wilddog.initializeApp(config); 242 | var auth = $wilddogAuth(wilddog.auth()); 243 | // 通过weixin登录 244 | auth.$authWithOAuthPopup("weixin").then(function(authData) { 245 | console.log("uid : ", authData.uid); 246 | }).catch(function(error) { 247 | console.log("登录失败:", error); 248 | }); 249 | }); 250 | 251 | ``` 252 | 253 | ## API 254 | 255 | [API文档](https://github.com/WildDogTeam/lib-js-wildangular/blob/master/API.md) 256 | 257 | ## Guide 258 | 259 | 260 | 261 | 262 | ## Contributing 263 | 264 | TBD 265 | 266 | ```bash 267 | $ git clone https://github.com/WildDogTeam/lib-js-wildangular.git 268 | $ cd lib-js-wildangular 269 | $ npm install -g grunt-cli 270 | $ npm install 271 | $ grunt install 272 | $ grunt watch 273 | ``` 274 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wild-angular", 3 | "description": "AnguarJS 支持的官方版本", 4 | "version": "1.0.0", 5 | "authors": [ 6 | "Wilddog(https://www.wilddog.com/)" 7 | ], 8 | "homepage": "https://github.com/WilddogTeam/lib-js-wildangular", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/WilddogTeam/lib-js-wildangular.git" 12 | }, 13 | "license": "MIT", 14 | "keywords": [ 15 | "angular", 16 | "angularjs", 17 | "wilddog", 18 | "realtime" 19 | ], 20 | "main": "dist/wild-angular.js", 21 | "ignore": [ 22 | "**/*", 23 | "!dist/*.js", 24 | "!README.md", 25 | "!LICENSE" 26 | ], 27 | "dependencies": { 28 | "angular": "1.3.x || 1.4.x", 29 | "wilddog": "^0.4.8" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /dist/wild-angular.min.js: -------------------------------------------------------------------------------- 1 | !function(a){"use strict";angular.module("wilddog",[]).value("Wilddog",a.Wilddog)}(window),function(){"use strict";angular.module("wilddog").factory("$wilddogArray",["$log","$wilddogUtils","$q",function(a,b,c){function d(a){if(!(this instanceof d))return new d(a);var c=this;return this._observers=[],this.$list=[],this._ref=a,this._sync=new e(this),b.assertValidRef(a,"Must pass a valid Wilddog reference to $wilddogArray (not a string or URL)"),this._indexCache={},b.getPublicMethods(c,function(a,b){c.$list[b]=a.bind(c)}),this._sync.init(this.$list),this.$list}function e(d){function e(a){if(!r.isDestroyed){r.isDestroyed=!0;var b=d.$ref();b.off("child_added",j),b.off("child_moved",l),b.off("child_changed",k),b.off("child_removed",m),d=null,q(a||"destroyed")}}function f(b){var c=d.$ref();c.on("child_added",j,p),c.on("child_moved",l,p),c.on("child_changed",k,p),c.on("child_removed",m,p),c.once("value",function(c){angular.isArray(c.val())&&a.warn("Storing data using array indices in Wilddog can result in unexpected behavior. See https://www.wilddog.com/docs/web/guide/understanding-data.html#section-arrays-in-wilddog for more information."),q(null,b)},q)}function g(a,b){o||(o=!0,a?i.reject(a):i.resolve(b))}function h(a,b){var d=c.when(a);d.then(function(a){a&&b(a)}),o||n.push(d)}var i=b.defer(),j=function(a,b){h(d.$$added(a,b),function(a){d.$$process("child_added",a,b)})},k=function(a){var c=d.$getRecord(b.getKey(a));c&&h(d.$$updated(a),function(){d.$$process("child_changed",c)})},l=function(a,c){var e=d.$getRecord(b.getKey(a));e&&h(d.$$moved(a,c),function(){d.$$process("child_moved",e,c)})},m=function(a){var c=d.$getRecord(b.getKey(a));c&&h(d.$$removed(a),function(){d.$$process("child_removed",c)})},n=[],o=!1,p=b.batch(function(a){g(a),d&&d.$$error(a)}),q=b.batch(g),r={destroy:e,isDestroyed:!1,init:f,ready:function(){return i.promise.then(function(a){return c.all(n).then(function(){return a})})}};return r}return d.prototype={$add:function(a){this._assertNotDestroyed("$add");var c=b.defer(),d=this.$ref().ref().push();return d.set(b.toJSON(a),b.makeNodeResolver(c)),c.promise.then(function(){return d})},$save:function(a){this._assertNotDestroyed("$save");var c=this,d=c._resolveItem(a),e=c.$keyAt(d);if(null!==e){var f=c.$ref().ref().child(e),g=b.toJSON(d);return b.doSet(f,g).then(function(){return c.$$notify("child_changed",e),f})}return b.reject("Invalid record; could determine key for "+a)},$remove:function(a){this._assertNotDestroyed("$remove");var c=this.$keyAt(a);if(null!==c){var d=this.$ref().ref().child(c);return b.doRemove(d).then(function(){return d})}return b.reject("Invalid record; could not determine key for "+a)},$keyAt:function(a){var b=this._resolveItem(a);return this.$$getKey(b)},$indexFor:function(a){var b=this,c=b._indexCache;if(!c.hasOwnProperty(a)||b.$keyAt(c[a])!==a){var d=b.$list.findIndex(function(c){return b.$$getKey(c)===a});d!==-1&&(c[a]=d)}return c.hasOwnProperty(a)?c[a]:-1},$loaded:function(a,b){var c=this._sync.ready();return arguments.length&&(c=c.then.call(c,a,b)),c},$ref:function(){return this._ref},$watch:function(a,b){var c=this._observers;return c.push([a,b]),function(){var d=c.findIndex(function(c){return c[0]===a&&c[1]===b});d>-1&&c.splice(d,1)}},$destroy:function(a){this._isDestroyed||(this._isDestroyed=!0,this._sync.destroy(a),this.$list.length=0)},$getRecord:function(a){var b=this.$indexFor(a);return b>-1?this.$list[b]:null},$$added:function(a){if(this.$indexFor(b.getKey(a))===-1){var c=a.val();return angular.isObject(c)||(c={$value:c}),c.$id=b.getKey(a),c.$priority=a.getPriority(),b.applyDefaults(c,this.$$defaults),c}return!1},$$removed:function(a){return this.$indexFor(b.getKey(a))>-1},$$updated:function(a){var c=!1,d=this.$getRecord(b.getKey(a));return angular.isObject(d)&&(c=b.updateRec(d,a),b.applyDefaults(d,this.$$defaults)),c},$$moved:function(a){var c=this.$getRecord(b.getKey(a));return!!angular.isObject(c)&&(c.$priority=a.getPriority(),!0)},$$error:function(b){a.error(b),this.$destroy(b)},$$getKey:function(a){return angular.isObject(a)?a.$id:null},$$process:function(a,b,c){var d,e=this.$$getKey(b),f=!1;switch(a){case"child_added":d=this.$indexFor(e);break;case"child_moved":d=this.$indexFor(e),this._spliceOut(e);break;case"child_removed":f=null!==this._spliceOut(e);break;case"child_changed":f=!0;break;default:throw new Error("Invalid event type: "+a)}return angular.isDefined(d)&&(f=this._addAfter(b,c)!==d),f&&this.$$notify(a,e,c),f},$$notify:function(a,b,c){var d={event:a,key:b};angular.isDefined(c)&&(d.prevChild=c),angular.forEach(this._observers,function(a){a[0].call(a[1],d)})},_addAfter:function(a,b){var c;return null===b?c=0:0===(c=this.$indexFor(b)+1)&&(c=this.$list.length),this.$list.splice(c,0,a),this._indexCache[this.$$getKey(a)]=c,c},_spliceOut:function(a){var b=this.$indexFor(a);return b>-1?(delete this._indexCache[a],this.$list.splice(b,1)[0]):null},_resolveItem:function(a){var b=this.$list;if(angular.isNumber(a)&&a>=0&&b.length>=a)return b[a];if(angular.isObject(a)){var c=this.$$getKey(a),d=this.$getRecord(c);return d===a?d:null}return null},_assertNotDestroyed:function(a){if(this._isDestroyed)throw new Error("Cannot call "+a+" method on a destroyed $wilddogArray object")}},d.$extend=function(a,c){return 1===arguments.length&&angular.isObject(a)&&(c=a,a=function(b){return this instanceof a?(d.apply(this,arguments),this.$list):new a(b)}),b.inherit(a,d,c)},d}]),angular.module("wilddog").factory("$WilddogArray",["$log","$wilddogArray",function(a,b){return function(){return a.warn("$WilddogArray has been renamed. Use $wilddogArray instead."),b.apply(null,arguments)}}])}(),function(){"use strict";var a;angular.module("wilddog").factory("$wilddogAuth",["$q","$wilddogUtils",function(b,c){return function(d){return new a(b,c,d).construct()}}]),a=function(a,b,c){if(this._q=a,this._utils=b,"string"==typeof c)throw new Error("Please provide a Wilddog auth instead of a URL when creating a `$wilddogAuth` object.");if(void 0!==c.ref)throw new Error("Please provide a Wilddog auth instead of a Wilddog sync when creating a `$wilddogAuth` object.");this._auth=c,this._initialAuthResolver=this._initAuthResolver()},a.prototype={construct:function(){return this._object={$signInWithCustomToken:this.signInWithCustomToken.bind(this),$signInAnonymously:this.signInAnonymously.bind(this),$signInWithEmailAndPassword:this.signInWithEmailAndPassword.bind(this),$signInWithPhoneAndPassword:this.signInWithPhoneAndPassword.bind(this),$signInWithPopup:this.signInWithPopup.bind(this),$signInWithRedirect:this.signInWithRedirect.bind(this),$signInWithCredential:this.signInWithCredential.bind(this),$signOut:this.signOut.bind(this),$onAuthStateChanged:this.onAuthStateChanged.bind(this),$getUser:this.getUser.bind(this),$requireUser:this.requireUser.bind(this),$waitForSignIn:this.waitForSignIn.bind(this),$createUserWithPhoneAndPassword:this.createUserWithPhoneAndPassword.bind(this),$createUserWithEmailAndPassword:this.createUserWithEmailAndPassword.bind(this),$sendPasswordResetEmail:this.sendPasswordResetEmail.bind(this),$sendPasswordResetPhone:this.sendPasswordResetPhone.bind(this),$updateProfile:this.updateProfile.bind(this),$updatePassword:this.updatePassword.bind(this),$updateEmail:this.updateEmail.bind(this),$updatePhone:this.updatePhone.bind(this),$deleteUser:this.deleteUser.bind(this)},this._object},signInWithCustomToken:function(a){return this._q.when(this._auth.signInWithCustomToken(a))},signInAnonymously:function(){return this._q.when(this._auth.signInAnonymously())},signInWithEmailAndPassword:function(a,b){return this._q.when(this._auth.signInWithEmailAndPassword(a,b))},signInWithPhoneAndPassword:function(a,b){return this._q.when(this._auth.signInWithPhoneAndPassword(a,b))},signInWithPopup:function(a){return this._q.when(this._auth.signInWithPopup(a))},signInWithRedirect:function(a){return this._q.when(this._auth.signInWithRedirect(a))},signInWithCredential:function(a){return this._q.when(this._auth.signInWithCredential(a))},signOut:function(){return null!==this.getUser()?this._q.when(this._auth.signOut()):this._q.when()},onAuthStateChanged:function(a,b){var c=this._utils.debounce(a,b,0);return this._auth.onAuthStateChanged(c)},getUser:function(){return this._auth.currentUser},_routerMethodOnAuthPromise:function(a,b){var c=this;return this._initialAuthResolver.then(function(){var d=c.getUser();return a&&null===d?c._q.reject("AUTH_REQUIRED"):b&&!d.emailVerified?c._q.reject("EMAIL_VERIFICATION_REQUIRED"):c._q.when(d)})},_initAuthResolver:function(){var a=this._auth;return this._q(function(b){function c(){setTimeout(function(){d(),b()},0)}var d;d=a.onAuthStateChanged(c)})},requireUser:function(a){return this._routerMethodOnAuthPromise(!0,a)},waitForSignIn:function(){return this._routerMethodOnAuthPromise(!1,!1)},createUserWithPhoneAndPassword:function(a,b){return this._q.when(this._auth.createUserWithPhoneAndPassword(a,b))},createUserWithEmailAndPassword:function(a,b){return this._q.when(this._auth.createUserWithEmailAndPassword(a,b))},updateProfile:function(a){var b=this.getUser();return b?this._q.when(b.updateProfile(a)):this._q.reject("Cannot update profile since there is no logged in user.")},updatePassword:function(a){var b=this.getUser();return b?this._q.when(b.updatePassword(a)):this._q.reject("Cannot update password since there is no logged in user.")},updatePhone:function(a){var b=this.getUser();return b?this._q.when(b.updatePhone(a)):this._q.reject("Cannot update phone since there is no logged in user.")},updateEmail:function(a){var b=this.getUser();return b?this._q.when(b.updateEmail(a)):this._q.reject("Cannot update email since there is no logged in user.")},deleteUser:function(){var a=this.getUser();return a?this._q.when(a.delete()):this._q.reject("Cannot delete user since there is no logged in user.")},sendPasswordResetPhone:function(a){return this._q.when(this._auth.sendPasswordResetSms(a))},sendPasswordResetEmail:function(a){return this._q.when(this._auth.sendPasswordResetEmail(a))}}}(),function(){"use strict";angular.module("wilddog").factory("$wilddogObject",["$parse","$wilddogUtils","$log",function(a,b,c){function d(a){if(!(this instanceof d))return new d(a);this.$$conf={sync:new f(this,a),ref:a,binding:new e(this),listeners:[]},Object.defineProperty(this,"$$conf",{value:this.$$conf}),this.$id=b.getKey(a.ref()),this.$priority=null,b.applyDefaults(this,this.$$defaults),this.$$conf.sync.init()}function e(a){this.subs=[],this.scope=null,this.key=null,this.rec=a}function f(a,d){function e(b){m.isDestroyed||(m.isDestroyed=!0,d.off("value",j),a=null,l(b||"destroyed"))}function f(){d.on("value",j,k),d.once("value",function(a){angular.isArray(a.val())&&c.warn("Storing data using array indices in Wilddog can result in unexpected behavior. See https://www.wilddog.com/docs/web/guide/understanding-data.html#section-arrays-in-wilddog for more information. Also note that you probably wanted $wilddogArray and not $wilddogObject."),l(null)},l)}function g(b){h||(h=!0,b?i.reject(b):i.resolve(a))}var h=!1,i=b.defer(),j=b.batch(function(b){a.$$updated(b)&&a.$$notify()}),k=b.batch(function(b){g(b),a&&a.$$error(b)}),l=b.batch(g),m={isDestroyed:!1,destroy:e,init:f,ready:function(){return i.promise}};return m}return d.prototype={$save:function(){var a=this,c=a.$ref(),d=b.toJSON(a);return b.doSet(c,d).then(function(){return a.$$notify(),a.$ref()})},$remove:function(){var a=this;return b.trimKeys(a,{}),a.$value=null,b.doRemove(a.$ref()).then(function(){return a.$$notify(),a.$ref()})},$loaded:function(a,b){var c=this.$$conf.sync.ready();return arguments.length&&(c=c.then.call(c,a,b)),c},$ref:function(){return this.$$conf.ref},$bindTo:function(a,b){var c=this;return c.$loaded().then(function(){return c.$$conf.binding.bindTo(a,b)})},$watch:function(a,b){var c=this.$$conf.listeners;return c.push([a,b]),function(){var d=c.findIndex(function(c){return c[0]===a&&c[1]===b});d>-1&&c.splice(d,1)}},$destroy:function(a){var c=this;c.$isDestroyed||(c.$isDestroyed=!0,c.$$conf.sync.destroy(a),c.$$conf.binding.destroy(),b.each(c,function(a,b){delete c[b]}))},$$updated:function(a){var c=b.updateRec(this,a);return b.applyDefaults(this,this.$$defaults),c},$$error:function(a){c.error(a),this.$destroy(a)},$$scopeUpdated:function(a){var c=b.defer();return this.$ref().set(b.toJSON(a),b.makeNodeResolver(c)),c.promise},$$notify:function(){var a=this,b=this.$$conf.listeners.slice();angular.forEach(b,function(b){b[0].call(b[1],{event:"value",key:a.$id})})},forEach:function(a,c){return b.each(this,a,c)}},d.$extend=function(a,c){return 1===arguments.length&&angular.isObject(a)&&(c=a,a=function(b){if(!(this instanceof a))return new a(b);d.apply(this,arguments)}),b.inherit(a,d,c)},e.prototype={assertNotBound:function(a){if(this.scope){var d="Cannot bind to "+a+" because this instance is already bound to "+this.key+"; one binding per instance (call unbind method or create another WilddogObject instance)";return c.error(d),b.reject(d)}},bindTo:function(c,d){function e(e){function f(a){return angular.equals(a,k)&&a.$priority===k.$priority&&a.$value===k.$value}function g(a){j.assign(c,b.scopeData(a))}function h(){var a=j(c);return[a,a.$priority,a.$value]}var i=!1,j=a(d),k=e.rec;e.scope=c,e.varName=d;var l=b.debounce(function(a){var d=b.scopeData(a);k.$$scopeUpdated(d).finally(function(){i=!1,d.hasOwnProperty("$value")||(delete k.$value,delete j(c).$value)})},50,500),m=function(a){a=a[0],f(a)||(i=!0,l(a))},n=function(){i||f(j(c))||g(k)};return g(k),e.subs.push(c.$on("$destroy",e.unbind.bind(e))),e.subs.push(c.$watch(h,m,!0)),e.subs.push(k.$watch(n)),e.unbind.bind(e)}return this.assertNotBound(d)||e(this)},unbind:function(){this.scope&&(angular.forEach(this.subs,function(a){a()}),this.subs=[],this.scope=null,this.key=null)},destroy:function(){this.unbind(),this.rec=null}},d}]),angular.module("wilddog").factory("$WilddogObject",["$log","$wilddogObject",function(a,b){return function(){return a.warn("$WilddogObject has been renamed. Use $wilddogObject instead."),b.apply(null,arguments)}}])}(),Array.prototype.indexOf||(Array.prototype.indexOf=function(a,b){if(void 0===this||null===this)throw new TypeError("'this' is null or not defined");var c=this.length>>>0;for(b=+b||0,Math.abs(b)===1/0&&(b=0),b<0&&(b+=c)<0&&(b=0);b>>0,e=arguments[1],f=0;f1)throw new Error("Second argument not supported");if(null===b)throw new Error("Cannot set a null [[Prototype]]");if("object"!=typeof b)throw new TypeError("Argument must be an object");return a.prototype=b,new a}}(),Object.keys||(Object.keys=function(){"use strict";var a=Object.prototype.hasOwnProperty,b=!{toString:null}.propertyIsEnumerable("toString"),c=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],d=c.length;return function(e){if("object"!=typeof e&&("function"!=typeof e||null===e))throw new TypeError("Object.keys called on non-object");var f,g,h=[];for(f in e)a.call(e,f)&&h.push(f);if(b)for(g=0;gd?l||(l=!0,f.compile(g)):(i||(i=Date.now()),j=f.wait(g,c))}function g(){j=null,i=null,l=!1,a.apply(b,k)}function h(){k=Array.prototype.slice.call(arguments,0),e()}var i,j,k,l;if("number"==typeof b&&(d=c,c=b,b=null),"number"!=typeof c)throw new Error("Must provide a valid integer for wait. Try 0 for a default");if("function"!=typeof a)throw new Error("Must provide a valid function to debounce");return d||(d=10*c||100),h.running=function(){return i>0},h},assertValidRef:function(a,b){if(!angular.isObject(a)||"function"!=typeof a.ref||"function"!=typeof a.ref().transaction)throw new Error(b||"Invalid Wilddog reference")},inherit:function(a,b,c){var d=a.prototype;return a.prototype=Object.create(b.prototype),a.prototype.constructor=a,angular.forEach(Object.keys(d),function(b){a.prototype[b]=d[b]}),angular.isObject(c)&&angular.extend(a.prototype,c),a},getPrototypeMethods:function(a,b,c){for(var d={},e=Object.getPrototypeOf({}),f=angular.isFunction(a)&&angular.isObject(a.prototype)?a.prototype:Object.getPrototypeOf(a);f&&f!==e;){for(var g in f)f.hasOwnProperty(g)&&!d.hasOwnProperty(g)&&(d[g]=!0,b.call(c,f[g],g,f));f=Object.getPrototypeOf(f)}},getPublicMethods:function(a,b,c){f.getPrototypeMethods(a,function(a,d){"function"==typeof a&&"_"!==d.charAt(0)&&b.call(c,a,d)})},defer:b.defer,reject:b.reject,resolve:b.when,promise:angular.isFunction(b)?b:e,makeNodeResolver:function(a){return function(b,c){null===b?(arguments.length>2&&(c=Array.prototype.slice.call(arguments,1)),a.resolve(c)):a.reject(b)}},wait:function(a,b){var d=c(a,b||0);return function(){d&&(c.cancel(d),d=null)}},compile:function(a){return d.$evalAsync(a||function(){})},deepCopy:function(a){if(!angular.isObject(a))return a;var b=angular.isArray(a)?a.slice():angular.extend({},a);for(var c in b)b.hasOwnProperty(c)&&angular.isObject(b[c])&&(b[c]=f.deepCopy(b[c]));return b},trimKeys:function(a,b){f.each(a,function(c,d){b.hasOwnProperty(d)||delete a[d]})},scopeData:function(a){var b={$id:a.$id,$priority:a.$priority},c=!1;return f.each(a,function(a,d){c=!0,b[d]=f.deepCopy(a)}),!c&&a.hasOwnProperty("$value")&&(b.$value=a.$value),b},updateRec:function(a,b){var c=b.val(),d=angular.extend({},a);return angular.isObject(c)?delete a.$value:(a.$value=c,c={}),f.trimKeys(a,c),angular.extend(a,c),a.$priority=b.getPriority(),!angular.equals(d,a)||d.$value!==a.$value||d.$priority!==a.$priority},applyDefaults:function(a,b){return angular.isObject(b)&&angular.forEach(b,function(b,c){a.hasOwnProperty(c)||(a[c]=b)}),a},dataKeys:function(a){var b=[];return f.each(a,function(a,c){b.push(c)}),b},each:function(a,b,c){if(angular.isObject(a)){for(var d in a)if(a.hasOwnProperty(d)){var e=d.charAt(0);"_"!==e&&"$"!==e&&"."!==e&&b.call(c,a[d],d,a)}}else if(angular.isArray(a))for(var f=0,g=a.length;f0&&null!==b.$priority&&(c[".priority"]=b.$priority),angular.forEach(c,function(a,b){if(b.match(/[.$\[\]#\/]/)&&".value"!==b&&".priority"!==b)throw new Error("Invalid key "+b+" (cannot contain .$[]#)");if(angular.isUndefined(a))throw new Error("Key "+b+" was undefined. Cannot pass undefined in JSON. Use null instead.")}),c},doSet:function(a,b){var c=f.defer();if(angular.isFunction(a.set)||!angular.isObject(b))a.set(b,f.makeNodeResolver(c));else{var d=angular.extend({},b);a.once("value",function(b){b.forEach(function(a){d.hasOwnProperty(f.getKey(a))||(d[f.getKey(a)]=null)}),a.ref().update(d,f.makeNodeResolver(c))},function(a){c.reject(a)})}return c.promise},doRemove:function(a){var b=f.defer();return angular.isFunction(a.remove)?a.remove(f.makeNodeResolver(b)):a.once("value",function(c){var d=[];c.forEach(function(a){var c=f.defer();d.push(c.promise),a.ref().remove(f.makeNodeResolver(b))}),f.allPromises(d).then(function(){b.resolve(a)},function(a){b.reject(a)})},function(a){b.reject(a)}),b.promise},VERSION:"0.0.0",allPromises:b.all.bind(b)};return f}])}(),function(){"use strict";angular.module("wilddog").factory("$wilddog",function(){return function(){throw new Error("$wilddog has been removed. You may instantiate $wilddogArray and $wilddogObject directly now. For simple write operations, just use the Wilddog ref directly. See the wild-angular 1.0.0 changelog for details: https://www.wilddog.com/docs/web/libraries/angular/changelog.html")}})}(); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('./dist/wild-angular'); 2 | module.exports = 'wilddog'; 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wild-angular", 3 | "description": "The officially supported AngularJS binding for Wilddog", 4 | "version": "1.0.0", 5 | "author": "Wilddog (https://www.wilddog.com/)", 6 | "homepage": "https://github.com/WilddogTeam/lib-js-wildangular", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/WilddogTeam/lib-js-wildangular.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/WilddogTeam/lib-js-wildangular/issues" 13 | }, 14 | "license": "MIT", 15 | "keywords": [ 16 | "angular", 17 | "angularjs", 18 | "wilddog", 19 | "realtime" 20 | ], 21 | "main": "index.js", 22 | "files": [ 23 | "index.js", 24 | "dist/**", 25 | "LICENSE", 26 | "README.md", 27 | "package.json" 28 | ], 29 | "dependencies": { 30 | "angular": "1.3.x || 1.4.x", 31 | "wilddog": "^2.5.2" 32 | }, 33 | "devDependencies": { 34 | "angular-mocks": "~1.4.6", 35 | "coveralls": "^2.11.2", 36 | "grunt": "~0.4.5", 37 | "grunt-cli": "^0.1.13", 38 | "grunt-contrib-concat": "^0.5.0", 39 | "grunt-contrib-connect": "^0.9.0", 40 | "grunt-contrib-jshint": "^0.11.0", 41 | "grunt-contrib-uglify": "^0.7.0", 42 | "grunt-contrib-watch": "^0.6.1", 43 | "grunt-karma": "^0.10.1", 44 | "grunt-notify": "^0.4.1", 45 | "grunt-protractor-runner": "^1.2.1", 46 | "grunt-shell-spawn": "^0.3.1", 47 | "jasmine-core": "^2.2.0", 48 | "jasmine-spec-reporter": "^2.1.0", 49 | "karma": "~0.12.31", 50 | "karma-chrome-launcher": "^0.1.7", 51 | "karma-coverage": "^0.2.7", 52 | "karma-failed-reporter": "0.0.3", 53 | "karma-firefox-launcher": "^0.1.4", 54 | "karma-html2js-preprocessor": "~0.1.0", 55 | "karma-jasmine": "^0.3.5", 56 | "karma-phantomjs-launcher": "~0.1.4", 57 | "karma-sauce-launcher": "~0.2.10", 58 | "karma-spec-reporter": "0.0.16", 59 | "load-grunt-tasks": "^3.1.0", 60 | "protractor": "^1.6.1" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/WilddogAuth.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | var WilddogAuth; 4 | 5 | // Define a service which provides user authentication and management. 6 | angular.module('wilddog').factory('$wilddogAuth', [ 7 | '$q', '$wilddogUtils', 8 | function($q, $wilddogUtils) { 9 | /** 10 | * This factory returns an object allowing you to manage the client's authentication state. 11 | * 12 | * @param {Wilddog} auth A Wilddog Auth to authenticate. 13 | * @return {object} An object containing methods for authenticating clients, retrieving 14 | * authentication state, and managing users. 15 | */ 16 | return function(auth) { 17 | var authInstance = new WilddogAuth($q, $wilddogUtils, auth); 18 | return authInstance.construct(); 19 | }; 20 | } 21 | ]); 22 | 23 | WilddogAuth = function($q, $wilddogUtils, auth) { 24 | this._q = $q; 25 | this._utils = $wilddogUtils; 26 | if (typeof auth === 'string') { 27 | throw new Error('Please provide a Wilddog auth instead of a URL when creating a `$wilddogAuth` object.'); 28 | } else if (typeof auth.ref !== 'undefined') { 29 | throw new Error('Please provide a Wilddog auth instead of a Wilddog sync when creating a `$wilddogAuth` object.'); 30 | } 31 | this._auth = auth; 32 | this._initialAuthResolver = this._initAuthResolver(); 33 | }; 34 | 35 | WilddogAuth.prototype = { 36 | construct: function() { 37 | this._object = { 38 | // Authentication methods 39 | $signInWithCustomToken: this.signInWithCustomToken.bind(this), 40 | $signInAnonymously: this.signInAnonymously.bind(this), 41 | $signInWithEmailAndPassword: this.signInWithEmailAndPassword.bind(this), 42 | $signInWithPhoneAndPassword: this.signInWithPhoneAndPassword.bind(this), 43 | $signInWithPopup: this.signInWithPopup.bind(this), 44 | $signInWithRedirect: this.signInWithRedirect.bind(this), 45 | $signInWithCredential: this.signInWithCredential.bind(this), 46 | $signOut: this.signOut.bind(this), 47 | 48 | // Authentication state methods 49 | $onAuthStateChanged: this.onAuthStateChanged.bind(this), 50 | $getUser: this.getUser.bind(this), 51 | $requireUser: this.requireUser.bind(this), 52 | $waitForSignIn: this.waitForSignIn.bind(this), 53 | 54 | // User management methods 55 | $createUserWithPhoneAndPassword: this.createUserWithPhoneAndPassword.bind(this), 56 | $createUserWithEmailAndPassword: this.createUserWithEmailAndPassword.bind(this), 57 | $sendPasswordResetEmail: this.sendPasswordResetEmail.bind(this), 58 | $sendPasswordResetPhone: this.sendPasswordResetPhone.bind(this), 59 | $updateProfile: this.updateProfile.bind(this), 60 | $updatePassword: this.updatePassword.bind(this), 61 | $updateEmail: this.updateEmail.bind(this), 62 | $updatePhone: this.updatePhone.bind(this), 63 | $deleteUser: this.deleteUser.bind(this), 64 | //************************ 65 | 66 | // // Authentication methods 67 | // $authWithCustomToken: this.authWithCustomToken.bind(this), 68 | // $authAnonymously: this.authAnonymously.bind(this), 69 | // $authWithPassword: this.authWithPassword.bind(this), 70 | // $authWithOAuthPopup: this.authWithOAuthPopup.bind(this), 71 | // $authWithOAuthRedirect: this.authWithOAuthRedirect.bind(this), 72 | // $authWithOAuthToken: this.authWithOAuthToken.bind(this), 73 | // $unauth: this.unauth.bind(this), 74 | // 75 | // // Authentication state methods 76 | // $onAuth: this.onAuth.bind(this), 77 | // $getUser: this.getUser.bind(this), 78 | // $requireAuth: this.requireAuth.bind(this), 79 | // $waitForAuth: this.waitForAuth.bind(this), 80 | // 81 | // // User management methods 82 | // $createUser: this.createUser.bind(this), 83 | // $updatePassword: this.updatePassword.bind(this), 84 | // $updateEmail: this.updateEmail.bind(this), 85 | // $deleteUser: this.deleteUser.bind(this), 86 | // $resetPassword: this.resetPassword.bind(this) 87 | }; 88 | 89 | return this._object; 90 | }, 91 | 92 | /********************/ 93 | /* Authentication */ 94 | /********************/ 95 | 96 | /** 97 | * Authenticates the Firebase reference with a custom authentication token. 98 | * 99 | * @param {string} authToken An authentication token or a Firebase Secret. A Firebase Secret 100 | * should only be used for authenticating a server process and provides full read / write 101 | * access to the entire Firebase. 102 | * @return {Promise} A promise fulfilled with an object containing authentication data. 103 | */ 104 | signInWithCustomToken: function(authToken) { 105 | return this._q.when(this._auth.signInWithCustomToken(authToken)); 106 | }, 107 | 108 | /** 109 | * Authenticates the Firebase reference anonymously. 110 | * 111 | * @return {Promise} A promise fulfilled with an object containing authentication data. 112 | */ 113 | signInAnonymously: function() { 114 | return this._q.when(this._auth.signInAnonymously()); 115 | }, 116 | 117 | /** 118 | * Authenticates the Firebase reference with an email/password user. 119 | * 120 | * @param {String} email An email address for the new user. 121 | * @param {String} password A password for the new email. 122 | * @return {Promise} A promise fulfilled with an object containing authentication data. 123 | */ 124 | signInWithEmailAndPassword: function(email, password) { 125 | return this._q.when(this._auth.signInWithEmailAndPassword(email, password)); 126 | }, 127 | 128 | /** 129 | * Authenticates the Firebase reference with an phone/password user. 130 | * 131 | * @param {String} phone An phone address for the new user. 132 | * @param {String} password A password for the new phone. 133 | * @return {Promise} A promise fulfilled with an object containing authentication data. 134 | */ 135 | signInWithPhoneAndPassword: function(phone, password) { 136 | return this._q.when(this._auth.signInWithPhoneAndPassword(phone, password)); 137 | }, 138 | /** 139 | * Authenticates the Firebase reference with the OAuth popup flow. 140 | * 141 | * @param {object|string} provider A wilddog.auth.AuthProvider or a unique provider ID like 'facebook'. 142 | * @return {Promise} A promise fulfilled with an object containing authentication data. 143 | */ 144 | signInWithPopup: function(provider) { 145 | return this._q.when(this._auth.signInWithPopup(provider)); 146 | }, 147 | 148 | /** 149 | * Authenticates the Firebase reference with the OAuth redirect flow. 150 | * 151 | * @param {object|string} provider A wilddog.auth.AuthProvider or a unique provider ID like 'facebook'. 152 | * @return {Promise} A promise fulfilled with an object containing authentication data. 153 | */ 154 | signInWithRedirect: function(provider) { 155 | return this._q.when(this._auth.signInWithRedirect(provider)); 156 | }, 157 | 158 | /** 159 | * Authenticates the Firebase reference with an OAuth token. 160 | * 161 | * @param {wilddog.auth.AuthCredential} credential The Firebase credential. 162 | * @return {Promise} A promise fulfilled with an object containing authentication data. 163 | */ 164 | signInWithCredential: function(credential) { 165 | return this._q.when(this._auth.signInWithCredential(credential)); 166 | }, 167 | 168 | /** 169 | * Unauthenticates the Firebase reference. 170 | */ 171 | signOut: function() { 172 | if (this.getUser() !== null) { 173 | return this._q.when(this._auth.signOut()); 174 | } else { 175 | return this._q.when(); 176 | } 177 | }, 178 | 179 | /**************************/ 180 | /* Authentication State */ 181 | /**************************/ 182 | /** 183 | * Asynchronously fires the provided callback with the current authentication data every time 184 | * the authentication data changes. It also fires as soon as the authentication data is 185 | * retrieved from the server. 186 | * 187 | * @param {function} callback A callback that fires when the client's authenticate state 188 | * changes. If authenticated, the callback will be passed an object containing authentication 189 | * data according to the provider used to authenticate. Otherwise, it will be passed null. 190 | * @param {string} [context] If provided, this object will be used as this when calling your 191 | * callback. 192 | * @return {Promise} A promised fulfilled with a function which can be used to 193 | * deregister the provided callback. 194 | */ 195 | onAuthStateChanged: function(callback, context) { 196 | var fn = this._utils.debounce(callback, context, 0); 197 | var off = this._auth.onAuthStateChanged(fn); 198 | 199 | // Return a method to detach the `onAuthStateChanged()` callback. 200 | return off; 201 | }, 202 | 203 | /** 204 | * Synchronously retrieves the current authentication data. 205 | * 206 | * @return {Object} The client's authentication data. 207 | */ 208 | getUser: function() { 209 | return this._auth.currentUser; 210 | }, 211 | 212 | /** 213 | * Helper onAuthStateChanged() callback method for the two router-related methods. 214 | * 215 | * @param {boolean} rejectIfAuthDataIsNull Determines if the returned promise should be 216 | * resolved or rejected upon an unauthenticated client. 217 | * @param {boolean} rejectIfEmailNotVerified Determines if the returned promise should be 218 | * resolved or rejected upon a client without a verified email address. 219 | * @return {Promise} A promise fulfilled with the client's authentication state or 220 | * rejected if the client is unauthenticated and rejectIfAuthDataIsNull is true. 221 | */ 222 | _routerMethodOnAuthPromise: function(rejectIfAuthDataIsNull, rejectIfEmailNotVerified) { 223 | var self = this; 224 | 225 | // wait for the initial auth state to resolve; on page load we have to request auth state 226 | // asynchronously so we don't want to resolve router methods or flash the wrong state 227 | return this._initialAuthResolver.then(function() { 228 | // auth state may change in the future so rather than depend on the initially resolved state 229 | // we also check the auth data (synchronously) if a new promise is requested, ensuring we resolve 230 | // to the current auth state and not a stale/initial state 231 | var authData = self.getUser(), 232 | res = null; 233 | if (rejectIfAuthDataIsNull && authData === null) { 234 | res = self._q.reject("AUTH_REQUIRED"); 235 | } else if (rejectIfEmailNotVerified && !authData.emailVerified) { 236 | res = self._q.reject("EMAIL_VERIFICATION_REQUIRED"); 237 | } else { 238 | res = self._q.when(authData); 239 | } 240 | return res; 241 | }); 242 | }, 243 | 244 | /** 245 | * Helper that returns a promise which resolves when the initial auth state has been 246 | * fetched from the Firebase server. This never rejects and resolves to undefined. 247 | * 248 | * @return {Promise} A promise fulfilled when the server returns initial auth state. 249 | */ 250 | _initAuthResolver: function() { 251 | var auth = this._auth; 252 | 253 | return this._q(function(resolve) { 254 | var off; 255 | 256 | function callback() { 257 | // Turn off this onAuthStateChanged() callback since we just needed to get the authentication data once. 258 | setTimeout(function () { 259 | off(); 260 | resolve(); 261 | }, 0); 262 | } 263 | off = auth.onAuthStateChanged(callback); 264 | }); 265 | }, 266 | 267 | /** 268 | * Utility method which can be used in a route's resolve() method to require that a route has 269 | * a logged in client. 270 | * 271 | * @param {boolean} requireEmailVerification Determines if the route requires a client with a 272 | * verified email address. 273 | * @returns {Promise} A promise fulfilled with the client's current authentication 274 | * state or rejected if the client is not authenticated. 275 | */ 276 | requireUser: function(requireEmailVerification) { 277 | return this._routerMethodOnAuthPromise(true, requireEmailVerification); 278 | }, 279 | 280 | /** 281 | * Utility method which can be used in a route's resolve() method to grab the current 282 | * authentication data. 283 | * 284 | * @returns {Promise} A promise fulfilled with the client's current authentication 285 | * state, which will be null if the client is not authenticated. 286 | */ 287 | waitForSignIn: function() { 288 | return this._routerMethodOnAuthPromise(false, false); 289 | }, 290 | 291 | /*********************/ 292 | /* User Management */ 293 | /*********************/ 294 | /** 295 | * Creates a new phone/password user. Note that this function only creates the user, if you 296 | * wish to log in as the newly created user, call $authWithPassword() after the promise for 297 | * this method has been resolved. 298 | * 299 | * @param {string} phone An phone number for this user. 300 | * @param {string} password A password for this user. 301 | * @return {Promise} A promise fulfilled with the user object, which contains the 302 | * uid of the created user. 303 | */ 304 | createUserWithPhoneAndPassword: function(phone, password) { 305 | return this._q.when(this._auth.createUserWithPhoneAndPassword(phone, password)); 306 | }, 307 | 308 | /*********************/ 309 | /* User Management */ 310 | /*********************/ 311 | /** 312 | * Creates a new email/password user. Note that this function only creates the user, if you 313 | * wish to log in as the newly created user, call $authWithPassword() after the promise for 314 | * this method has been resolved. 315 | * 316 | * @param {string} email An email for this user. 317 | * @param {string} password A password for this user. 318 | * @return {Promise} A promise fulfilled with the user object, which contains the 319 | * uid of the created user. 320 | */ 321 | createUserWithEmailAndPassword: function(email, password) { 322 | return this._q.when(this._auth.createUserWithEmailAndPassword(email, password)); 323 | }, 324 | 325 | /** 326 | * Changes the profile for an user. 327 | * 328 | * @param {string} profile A new profile for the current user. 329 | * @return {Promise<>} An empty promise fulfilled once the profile change is complete. 330 | */ 331 | updateProfile: function(profile) { 332 | var user = this.getUser(); 333 | if (user) { 334 | return this._q.when(user.updateProfile(profile)); 335 | } else { 336 | return this._q.reject("Cannot update profile since there is no logged in user."); 337 | } 338 | }, 339 | 340 | /** 341 | * Changes the password for an email/password user. 342 | * 343 | * @param {string} password A new password for the current user. 344 | * @return {Promise<>} An empty promise fulfilled once the password change is complete. 345 | */ 346 | updatePassword: function(password) { 347 | var user = this.getUser(); 348 | if (user) { 349 | return this._q.when(user.updatePassword(password)); 350 | } else { 351 | return this._q.reject("Cannot update password since there is no logged in user."); 352 | } 353 | }, 354 | 355 | /** 356 | * Changes the phone for an phone/password user. 357 | * 358 | * @param {String} phone The new phone for the currently logged in user. 359 | * @return {Promise<>} An empty promise fulfilled once the phone change is complete. 360 | */ 361 | updatePhone: function(phone) { 362 | var user = this.getUser(); 363 | if (user) { 364 | return this._q.when(user.updatePhone(phone)); 365 | } else { 366 | return this._q.reject("Cannot update phone since there is no logged in user."); 367 | } 368 | }, 369 | 370 | /** 371 | * Changes the email for an email/password user. 372 | * 373 | * @param {String} email The new email for the currently logged in user. 374 | * @return {Promise<>} An empty promise fulfilled once the email change is complete. 375 | */ 376 | updateEmail: function(email) { 377 | var user = this.getUser(); 378 | if (user) { 379 | return this._q.when(user.updateEmail(email)); 380 | } else { 381 | return this._q.reject("Cannot update email since there is no logged in user."); 382 | } 383 | }, 384 | 385 | /** 386 | * Deletes the currently logged in user. 387 | * 388 | * @return {Promise<>} An empty promise fulfilled once the user is removed. 389 | */ 390 | deleteUser: function() { 391 | var user = this.getUser(); 392 | if (user) { 393 | return this._q.when(user.delete()); 394 | } else { 395 | return this._q.reject("Cannot delete user since there is no logged in user."); 396 | } 397 | }, 398 | 399 | /** 400 | * Sends a password reset phone to an phone/password user. 401 | * 402 | * @param {string} phone An phone address to send a password reset to. 403 | * @return {Promise<>} An empty promise fulfilled once the reset password phone is sent. 404 | */ 405 | sendPasswordResetPhone: function(phone) { 406 | return this._q.when(this._auth.sendPasswordResetSms(phone)); 407 | }, 408 | 409 | /** 410 | * Sends a password reset email to an email/password user. 411 | * 412 | * @param {string} email An email address to send a password reset to. 413 | * @return {Promise<>} An empty promise fulfilled once the reset password email is sent. 414 | */ 415 | sendPasswordResetEmail: function(email) { 416 | return this._q.when(this._auth.sendPasswordResetEmail(email)); 417 | } 418 | }; 419 | })(); 420 | -------------------------------------------------------------------------------- /src/WilddogObject.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | /** 4 | * Creates and maintains a synchronized object, with 2-way bindings between Angular and Wilddog. 5 | * 6 | * Implementations of this class are contracted to provide the following internal methods, 7 | * which are used by the synchronization process and 3-way bindings: 8 | * $$updated - called whenever a change occurs (a value event from Wilddog) 9 | * $$error - called when listeners are canceled due to a security error 10 | * $$notify - called to update $watch listeners and trigger updates to 3-way bindings 11 | * $ref - called to obtain the underlying Wilddog reference 12 | * 13 | * Instead of directly modifying this class, one should generally use the $extend 14 | * method to add or change how methods behave: 15 | * 16 | *

 17 |    * var ExtendedObject = $wilddogObject.$extend({
 18 |    *    // add a new method to the prototype
 19 |    *    foo: function() { return 'bar'; },
 20 |    * });
 21 |    *
 22 |    * var obj = new ExtendedObject(ref);
 23 |    * 
24 | */ 25 | angular.module('wilddog').factory('$wilddogObject', [ 26 | '$parse', '$wilddogUtils', '$log', 27 | function($parse, $wilddogUtils, $log) { 28 | /** 29 | * Creates a synchronized object with 2-way bindings between Angular and Wilddog. 30 | * 31 | * @param {Wilddog} ref 32 | * @returns {WilddogObject} 33 | * @constructor 34 | */ 35 | function WilddogObject(ref) { 36 | if( !(this instanceof WilddogObject) ) { 37 | return new WilddogObject(ref); 38 | } 39 | // These are private config props and functions used internally 40 | // they are collected here to reduce clutter in console.log and forEach 41 | this.$$conf = { 42 | // synchronizes data to Wilddog 43 | sync: new ObjectSyncManager(this, ref), 44 | // stores the Wilddog ref 45 | ref: ref, 46 | // synchronizes $scope variables with this object 47 | binding: new ThreeWayBinding(this), 48 | // stores observers registered with $watch 49 | listeners: [] 50 | }; 51 | 52 | // this bit of magic makes $$conf non-enumerable and non-configurable 53 | // and non-writable (its properties are still writable but the ref cannot be replaced) 54 | // we redundantly assign it above so the IDE can relax 55 | Object.defineProperty(this, '$$conf', { 56 | value: this.$$conf 57 | }); 58 | 59 | this.$id = $wilddogUtils.getKey(ref.ref()); 60 | this.$priority = null; 61 | 62 | $wilddogUtils.applyDefaults(this, this.$$defaults); 63 | 64 | // start synchronizing data with Wilddog 65 | this.$$conf.sync.init(); 66 | } 67 | 68 | WilddogObject.prototype = { 69 | /** 70 | * Saves all data on the WilddogObject back to Wilddog. 71 | * @returns a promise which will resolve after the save is completed. 72 | */ 73 | $save: function () { 74 | var self = this; 75 | var ref = self.$ref(); 76 | var data = $wilddogUtils.toJSON(self); 77 | return $wilddogUtils.doSet(ref, data).then(function() { 78 | self.$$notify(); 79 | return self.$ref(); 80 | }); 81 | }, 82 | 83 | /** 84 | * Removes all keys from the WilddogObject and also removes 85 | * the remote data from the server. 86 | * 87 | * @returns a promise which will resolve after the op completes 88 | */ 89 | $remove: function() { 90 | var self = this; 91 | $wilddogUtils.trimKeys(self, {}); 92 | self.$value = null; 93 | return $wilddogUtils.doRemove(self.$ref()).then(function() { 94 | self.$$notify(); 95 | return self.$ref(); 96 | }); 97 | }, 98 | 99 | /** 100 | * The loaded method is invoked after the initial batch of data arrives from the server. 101 | * When this resolves, all data which existed prior to calling $asObject() is now cached 102 | * locally in the object. 103 | * 104 | * As a shortcut is also possible to pass resolve/reject methods directly into this 105 | * method just as they would be passed to .then() 106 | * 107 | * @param {Function} resolve 108 | * @param {Function} reject 109 | * @returns a promise which resolves after initial data is downloaded from Wilddog 110 | */ 111 | $loaded: function(resolve, reject) { 112 | var promise = this.$$conf.sync.ready(); 113 | if (arguments.length) { 114 | // allow this method to be called just like .then 115 | // by passing any arguments on to .then 116 | promise = promise.then.call(promise, resolve, reject); 117 | } 118 | return promise; 119 | }, 120 | 121 | /** 122 | * @returns {Wilddog} the original Wilddog instance used to create this object. 123 | */ 124 | $ref: function () { 125 | return this.$$conf.ref; 126 | }, 127 | 128 | /** 129 | * Creates a 3-way data sync between this object, the Wilddog server, and a 130 | * scope variable. This means that any changes made to the scope variable are 131 | * pushed to Wilddog, and vice versa. 132 | * 133 | * If scope emits a $destroy event, the binding is automatically severed. Otherwise, 134 | * it is possible to unbind the scope variable by using the `unbind` function 135 | * passed into the resolve method. 136 | * 137 | * Can only be bound to one scope variable at a time. If a second is attempted, 138 | * the promise will be rejected with an error. 139 | * 140 | * @param {object} scope 141 | * @param {string} varName 142 | * @returns a promise which resolves to an unbind method after data is set in scope 143 | */ 144 | $bindTo: function (scope, varName) { 145 | var self = this; 146 | return self.$loaded().then(function () { 147 | return self.$$conf.binding.bindTo(scope, varName); 148 | }); 149 | }, 150 | 151 | /** 152 | * Listeners passed into this method are notified whenever a new change is received 153 | * from the server. Each invocation is sent an object containing 154 | * { type: 'value', key: 'my_wilddog_id' } 155 | * 156 | * This method returns an unbind function that can be used to detach the listener. 157 | * 158 | * @param {Function} cb 159 | * @param {Object} [context] 160 | * @returns {Function} invoke to stop observing events 161 | */ 162 | $watch: function (cb, context) { 163 | var list = this.$$conf.listeners; 164 | list.push([cb, context]); 165 | // an off function for cancelling the listener 166 | return function () { 167 | var i = list.findIndex(function (parts) { 168 | return parts[0] === cb && parts[1] === context; 169 | }); 170 | if (i > -1) { 171 | list.splice(i, 1); 172 | } 173 | }; 174 | }, 175 | 176 | /** 177 | * Informs $wilddog to stop sending events and clears memory being used 178 | * by this object (delete's its local content). 179 | */ 180 | $destroy: function(err) { 181 | var self = this; 182 | if (!self.$isDestroyed) { 183 | self.$isDestroyed = true; 184 | self.$$conf.sync.destroy(err); 185 | self.$$conf.binding.destroy(); 186 | $wilddogUtils.each(self, function (v, k) { 187 | delete self[k]; 188 | }); 189 | } 190 | }, 191 | 192 | /** 193 | * Called by $wilddog whenever an item is changed at the server. 194 | * This method must exist on any objectFactory passed into $wilddog. 195 | * 196 | * It should return true if any changes were made, otherwise `$$notify` will 197 | * not be invoked. 198 | * 199 | * @param {object} snap a Wilddog snapshot 200 | * @return {boolean} true if any changes were made. 201 | */ 202 | $$updated: function (snap) { 203 | // applies new data to this object 204 | var changed = $wilddogUtils.updateRec(this, snap); 205 | // applies any defaults set using $$defaults 206 | $wilddogUtils.applyDefaults(this, this.$$defaults); 207 | // returning true here causes $$notify to be triggered 208 | return changed; 209 | }, 210 | 211 | /** 212 | * Called whenever a security error or other problem causes the listeners to become 213 | * invalid. This is generally an unrecoverable error. 214 | * @param {Object} err which will have a `code` property and possibly a `message` 215 | */ 216 | $$error: function (err) { 217 | // prints an error to the console (via Angular's logger) 218 | $log.error(err); 219 | // frees memory and cancels any remaining listeners 220 | this.$destroy(err); 221 | }, 222 | 223 | /** 224 | * Called internally by $bindTo when data is changed in $scope. 225 | * Should apply updates to this record but should not call 226 | * notify(). 227 | */ 228 | $$scopeUpdated: function(newData) { 229 | // we use a one-directional loop to avoid feedback with 3-way bindings 230 | // since set() is applied locally anyway, this is still performant 231 | var def = $wilddogUtils.defer(); 232 | this.$ref().set($wilddogUtils.toJSON(newData), $wilddogUtils.makeNodeResolver(def)); 233 | return def.promise; 234 | }, 235 | 236 | /** 237 | * Updates any bound scope variables and 238 | * notifies listeners registered with $watch 239 | */ 240 | $$notify: function() { 241 | var self = this, list = this.$$conf.listeners.slice(); 242 | // be sure to do this after setting up data and init state 243 | angular.forEach(list, function (parts) { 244 | parts[0].call(parts[1], {event: 'value', key: self.$id}); 245 | }); 246 | }, 247 | 248 | /** 249 | * Overrides how Angular.forEach iterates records on this object so that only 250 | * fields stored in Wilddog are part of the iteration. To include meta fields like 251 | * $id and $priority in the iteration, utilize for(key in obj) instead. 252 | */ 253 | forEach: function(iterator, context) { 254 | return $wilddogUtils.each(this, iterator, context); 255 | } 256 | }; 257 | 258 | /** 259 | * This method allows WilddogObject to be copied into a new factory. Methods passed into this 260 | * function will be added onto the object's prototype. They can override existing methods as 261 | * well. 262 | * 263 | * In addition to passing additional methods, it is also possible to pass in a class function. 264 | * The prototype on that class function will be preserved, and it will inherit from 265 | * WilddogObject. It's also possible to do both, passing a class to inherit and additional 266 | * methods to add onto the prototype. 267 | * 268 | * Once a factory is obtained by this method, it can be passed into $wilddog as the 269 | * `objectFactory` parameter: 270 | * 271 | *

272 |        * var MyFactory = $wilddogObject.$extend({
273 |        *    // add a method onto the prototype that prints a greeting
274 |        *    getGreeting: function() {
275 |        *       return 'Hello ' + this.first_name + ' ' + this.last_name + '!';
276 |        *    }
277 |        * });
278 |        *
279 |        * // use our new factory in place of $wilddogObject
280 |        * var obj = $wilddog(ref, {objectFactory: MyFactory}).$asObject();
281 |        * 
282 | * 283 | * @param {Function} [ChildClass] a child class which should inherit WilddogObject 284 | * @param {Object} [methods] a list of functions to add onto the prototype 285 | * @returns {Function} a new factory suitable for use with $wilddog 286 | */ 287 | WilddogObject.$extend = function(ChildClass, methods) { 288 | if( arguments.length === 1 && angular.isObject(ChildClass) ) { 289 | methods = ChildClass; 290 | ChildClass = function(ref) { 291 | if( !(this instanceof ChildClass) ) { 292 | return new ChildClass(ref); 293 | } 294 | WilddogObject.apply(this, arguments); 295 | }; 296 | } 297 | return $wilddogUtils.inherit(ChildClass, WilddogObject, methods); 298 | }; 299 | 300 | /** 301 | * Creates a three-way data binding on a scope variable. 302 | * 303 | * @param {WilddogObject} rec 304 | * @returns {*} 305 | * @constructor 306 | */ 307 | function ThreeWayBinding(rec) { 308 | this.subs = []; 309 | this.scope = null; 310 | this.key = null; 311 | this.rec = rec; 312 | } 313 | 314 | ThreeWayBinding.prototype = { 315 | assertNotBound: function(varName) { 316 | if( this.scope ) { 317 | var msg = 'Cannot bind to ' + varName + ' because this instance is already bound to ' + 318 | this.key + '; one binding per instance ' + 319 | '(call unbind method or create another WilddogObject instance)'; 320 | $log.error(msg); 321 | return $wilddogUtils.reject(msg); 322 | } 323 | }, 324 | 325 | bindTo: function(scope, varName) { 326 | function _bind(self) { 327 | var sending = false; 328 | var parsed = $parse(varName); 329 | var rec = self.rec; 330 | self.scope = scope; 331 | self.varName = varName; 332 | 333 | function equals(scopeValue) { 334 | return angular.equals(scopeValue, rec) && 335 | scopeValue.$priority === rec.$priority && 336 | scopeValue.$value === rec.$value; 337 | } 338 | 339 | function setScope(rec) { 340 | parsed.assign(scope, $wilddogUtils.scopeData(rec)); 341 | } 342 | 343 | var send = $wilddogUtils.debounce(function(val) { 344 | var scopeData = $wilddogUtils.scopeData(val); 345 | rec.$$scopeUpdated(scopeData) 346 | ['finally'](function() { 347 | sending = false; 348 | if(!scopeData.hasOwnProperty('$value')){ 349 | delete rec.$value; 350 | delete parsed(scope).$value; 351 | } 352 | } 353 | ); 354 | }, 50, 500); 355 | 356 | var scopeUpdated = function(newVal) { 357 | newVal = newVal[0]; 358 | if( !equals(newVal) ) { 359 | sending = true; 360 | send(newVal); 361 | } 362 | }; 363 | 364 | var recUpdated = function() { 365 | if( !sending && !equals(parsed(scope)) ) { 366 | setScope(rec); 367 | } 368 | }; 369 | 370 | // $watch will not check any vars prefixed with $, so we 371 | // manually check $priority and $value using this method 372 | function watchExp(){ 373 | var obj = parsed(scope); 374 | return [obj, obj.$priority, obj.$value]; 375 | } 376 | 377 | setScope(rec); 378 | self.subs.push(scope.$on('$destroy', self.unbind.bind(self))); 379 | 380 | // monitor scope for any changes 381 | self.subs.push(scope.$watch(watchExp, scopeUpdated, true)); 382 | 383 | // monitor the object for changes 384 | self.subs.push(rec.$watch(recUpdated)); 385 | 386 | return self.unbind.bind(self); 387 | } 388 | 389 | return this.assertNotBound(varName) || _bind(this); 390 | }, 391 | 392 | unbind: function() { 393 | if( this.scope ) { 394 | angular.forEach(this.subs, function(unbind) { 395 | unbind(); 396 | }); 397 | this.subs = []; 398 | this.scope = null; 399 | this.key = null; 400 | } 401 | }, 402 | 403 | destroy: function() { 404 | this.unbind(); 405 | this.rec = null; 406 | } 407 | }; 408 | 409 | function ObjectSyncManager(wilddogObject, ref) { 410 | function destroy(err) { 411 | if( !sync.isDestroyed ) { 412 | sync.isDestroyed = true; 413 | ref.off('value', applyUpdate); 414 | wilddogObject = null; 415 | initComplete(err||'destroyed'); 416 | } 417 | } 418 | 419 | function init() { 420 | ref.on('value', applyUpdate, error); 421 | ref.once('value', function(snap) { 422 | if (angular.isArray(snap.val())) { 423 | $log.warn('Storing data using array indices in Wilddog can result in unexpected behavior. See https://www.wilddog.com/docs/web/guide/understanding-data.html#section-arrays-in-wilddog for more information. Also note that you probably wanted $wilddogArray and not $wilddogObject.'); 424 | } 425 | 426 | initComplete(null); 427 | }, initComplete); 428 | } 429 | 430 | // call initComplete(); do not call this directly 431 | function _initComplete(err) { 432 | if( !isResolved ) { 433 | isResolved = true; 434 | if( err ) { def.reject(err); } 435 | else { def.resolve(wilddogObject); } 436 | } 437 | } 438 | 439 | var isResolved = false; 440 | var def = $wilddogUtils.defer(); 441 | var applyUpdate = $wilddogUtils.batch(function(snap) { 442 | var changed = wilddogObject.$$updated(snap); 443 | if( changed ) { 444 | // notifies $watch listeners and 445 | // updates $scope if bound to a variable 446 | wilddogObject.$$notify(); 447 | } 448 | }); 449 | var error = $wilddogUtils.batch(function(err) { 450 | _initComplete(err); 451 | if( wilddogObject ) { 452 | wilddogObject.$$error(err); 453 | } 454 | }); 455 | var initComplete = $wilddogUtils.batch(_initComplete); 456 | 457 | var sync = { 458 | isDestroyed: false, 459 | destroy: destroy, 460 | init: init, 461 | ready: function() { return def.promise; } 462 | }; 463 | return sync; 464 | } 465 | 466 | return WilddogObject; 467 | } 468 | ]); 469 | 470 | /** @deprecated */ 471 | angular.module('wilddog').factory('$WilddogObject', ['$log', '$wilddogObject', 472 | function($log, $wilddogObject) { 473 | return function() { 474 | $log.warn('$WilddogObject has been renamed. Use $wilddogObject instead.'); 475 | return $wilddogObject.apply(null, arguments); 476 | }; 477 | } 478 | ]); 479 | })(); 480 | -------------------------------------------------------------------------------- /src/lib/polyfills.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Shim Array.indexOf for IE compatibility. 4 | if (!Array.prototype.indexOf) { 5 | Array.prototype.indexOf = function (searchElement, fromIndex) { 6 | if (this === undefined || this === null) { 7 | throw new TypeError("'this' is null or not defined"); 8 | } 9 | // Hack to convert object.length to a UInt32 10 | // jshint -W016 11 | var length = this.length >>> 0; 12 | fromIndex = +fromIndex || 0; 13 | // jshint +W016 14 | 15 | if (Math.abs(fromIndex) === Infinity) { 16 | fromIndex = 0; 17 | } 18 | 19 | if (fromIndex < 0) { 20 | fromIndex += length; 21 | if (fromIndex < 0) { 22 | fromIndex = 0; 23 | } 24 | } 25 | 26 | for (;fromIndex < length; fromIndex++) { 27 | if (this[fromIndex] === searchElement) { 28 | return fromIndex; 29 | } 30 | } 31 | 32 | return -1; 33 | }; 34 | } 35 | 36 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind 37 | if (!Function.prototype.bind) { 38 | Function.prototype.bind = function (oThis) { 39 | if (typeof this !== "function") { 40 | // closest thing possible to the ECMAScript 5 41 | // internal IsCallable function 42 | throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); 43 | } 44 | 45 | var aArgs = Array.prototype.slice.call(arguments, 1), 46 | fToBind = this, 47 | fNOP = function () {}, 48 | fBound = function () { 49 | return fToBind.apply(this instanceof fNOP && oThis 50 | ? this 51 | : oThis, 52 | aArgs.concat(Array.prototype.slice.call(arguments))); 53 | }; 54 | 55 | fNOP.prototype = this.prototype; 56 | fBound.prototype = new fNOP(); 57 | 58 | return fBound; 59 | }; 60 | } 61 | 62 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex 63 | if (!Array.prototype.findIndex) { 64 | Object.defineProperty(Array.prototype, 'findIndex', { 65 | enumerable: false, 66 | configurable: true, 67 | writable: true, 68 | value: function(predicate) { 69 | if (this == null) { 70 | throw new TypeError('Array.prototype.find called on null or undefined'); 71 | } 72 | if (typeof predicate !== 'function') { 73 | throw new TypeError('predicate must be a function'); 74 | } 75 | var list = Object(this); 76 | var length = list.length >>> 0; 77 | var thisArg = arguments[1]; 78 | var value; 79 | 80 | for (var i = 0; i < length; i++) { 81 | if (i in list) { 82 | value = list[i]; 83 | if (predicate.call(thisArg, value, i, list)) { 84 | return i; 85 | } 86 | } 87 | } 88 | return -1; 89 | } 90 | }); 91 | } 92 | 93 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create 94 | if (typeof Object.create != 'function') { 95 | (function () { 96 | var F = function () {}; 97 | Object.create = function (o) { 98 | if (arguments.length > 1) { 99 | throw new Error('Second argument not supported'); 100 | } 101 | if (o === null) { 102 | throw new Error('Cannot set a null [[Prototype]]'); 103 | } 104 | if (typeof o != 'object') { 105 | throw new TypeError('Argument must be an object'); 106 | } 107 | F.prototype = o; 108 | return new F(); 109 | }; 110 | })(); 111 | } 112 | 113 | // From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys 114 | if (!Object.keys) { 115 | Object.keys = (function () { 116 | 'use strict'; 117 | var hasOwnProperty = Object.prototype.hasOwnProperty, 118 | hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'), 119 | dontEnums = [ 120 | 'toString', 121 | 'toLocaleString', 122 | 'valueOf', 123 | 'hasOwnProperty', 124 | 'isPrototypeOf', 125 | 'propertyIsEnumerable', 126 | 'constructor' 127 | ], 128 | dontEnumsLength = dontEnums.length; 129 | 130 | return function (obj) { 131 | if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) { 132 | throw new TypeError('Object.keys called on non-object'); 133 | } 134 | 135 | var result = [], prop, i; 136 | 137 | for (prop in obj) { 138 | if (hasOwnProperty.call(obj, prop)) { 139 | result.push(prop); 140 | } 141 | } 142 | 143 | if (hasDontEnumBug) { 144 | for (i = 0; i < dontEnumsLength; i++) { 145 | if (hasOwnProperty.call(obj, dontEnums[i])) { 146 | result.push(dontEnums[i]); 147 | } 148 | } 149 | } 150 | return result; 151 | }; 152 | }()); 153 | } 154 | 155 | // http://ejohn.org/blog/objectgetprototypeof/ 156 | if ( typeof Object.getPrototypeOf !== "function" ) { 157 | if ( typeof "test".__proto__ === "object" ) { 158 | Object.getPrototypeOf = function(object){ 159 | return object.__proto__; 160 | }; 161 | } else { 162 | Object.getPrototypeOf = function(object){ 163 | // May break if the constructor has been tampered with 164 | return object.constructor.prototype; 165 | }; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/module.js: -------------------------------------------------------------------------------- 1 | (function(exports) { 2 | "use strict"; 3 | 4 | // Define the `wilddog` module under which all wild-angular 5 | // services will live. 6 | angular.module("wilddog", []) 7 | //todo use $window 8 | .value("Wilddog", exports.Wilddog); 9 | 10 | })(window); 11 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('wilddog') 5 | .factory('$wilddogConfig', ["$wilddogArray", "$wilddogObject", "$injector", 6 | function($wilddogArray, $wilddogObject, $injector) { 7 | return function(configOpts) { 8 | // make a copy we can modify 9 | var opts = angular.extend({}, configOpts); 10 | // look up factories if passed as string names 11 | if( typeof opts.objectFactory === 'string' ) { 12 | opts.objectFactory = $injector.get(opts.objectFactory); 13 | } 14 | if( typeof opts.arrayFactory === 'string' ) { 15 | opts.arrayFactory = $injector.get(opts.arrayFactory); 16 | } 17 | // extend defaults and return 18 | return angular.extend({ 19 | arrayFactory: $wilddogArray, 20 | objectFactory: $wilddogObject 21 | }, opts); 22 | }; 23 | } 24 | ]) 25 | 26 | .factory('$wilddogUtils', ["$q", "$timeout", "$rootScope", 27 | function($q, $timeout, $rootScope) { 28 | 29 | // ES6 style promises polyfill for angular 1.2.x 30 | // Copied from angular 1.3.x implementation: https://github.com/angular/angular.js/blob/v1.3.5/src/ng/q.js#L539 31 | function Q(resolver) { 32 | if (!angular.isFunction(resolver)) { 33 | throw new Error('missing resolver function'); 34 | } 35 | 36 | var deferred = $q.defer(); 37 | 38 | function resolveFn(value) { 39 | deferred.resolve(value); 40 | } 41 | 42 | function rejectFn(reason) { 43 | deferred.reject(reason); 44 | } 45 | 46 | resolver(resolveFn, rejectFn); 47 | 48 | return deferred.promise; 49 | } 50 | 51 | var utils = { 52 | /** 53 | * Returns a function which, each time it is invoked, will gather up the values until 54 | * the next "tick" in the Angular compiler process. Then they are all run at the same 55 | * time to avoid multiple cycles of the digest loop. Internally, this is done using $evalAsync() 56 | * 57 | * @param {Function} action 58 | * @param {Object} [context] 59 | * @returns {Function} 60 | */ 61 | batch: function(action, context) { 62 | return function() { 63 | var args = Array.prototype.slice.call(arguments, 0); 64 | utils.compile(function() { 65 | action.apply(context, args); 66 | }); 67 | }; 68 | }, 69 | 70 | /** 71 | * A rudimentary debounce method 72 | * @param {function} fn the function to debounce 73 | * @param {object} [ctx] the `this` context to set in fn 74 | * @param {int} wait number of milliseconds to pause before sending out after each invocation 75 | * @param {int} [maxWait] max milliseconds to wait before sending out, defaults to wait * 10 or 100 76 | */ 77 | debounce: function(fn, ctx, wait, maxWait) { 78 | var start, cancelTimer, args, runScheduledForNextTick; 79 | if( typeof(ctx) === 'number' ) { 80 | maxWait = wait; 81 | wait = ctx; 82 | ctx = null; 83 | } 84 | 85 | if( typeof wait !== 'number' ) { 86 | throw new Error('Must provide a valid integer for wait. Try 0 for a default'); 87 | } 88 | if( typeof(fn) !== 'function' ) { 89 | throw new Error('Must provide a valid function to debounce'); 90 | } 91 | if( !maxWait ) { maxWait = wait*10 || 100; } 92 | 93 | // clears the current wait timer and creates a new one 94 | // however, if maxWait is exceeded, calls runNow() on the next tick. 95 | function resetTimer() { 96 | if( cancelTimer ) { 97 | cancelTimer(); 98 | cancelTimer = null; 99 | } 100 | if( start && Date.now() - start > maxWait ) { 101 | if(!runScheduledForNextTick){ 102 | runScheduledForNextTick = true; 103 | utils.compile(runNow); 104 | } 105 | } 106 | else { 107 | if( !start ) { start = Date.now(); } 108 | cancelTimer = utils.wait(runNow, wait); 109 | } 110 | } 111 | 112 | // Clears the queue and invokes the debounced function with the most recent arguments 113 | function runNow() { 114 | cancelTimer = null; 115 | start = null; 116 | runScheduledForNextTick = false; 117 | fn.apply(ctx, args); 118 | } 119 | 120 | function debounced() { 121 | args = Array.prototype.slice.call(arguments, 0); 122 | resetTimer(); 123 | } 124 | debounced.running = function() { 125 | return start > 0; 126 | }; 127 | 128 | return debounced; 129 | }, 130 | 131 | assertValidRef: function(ref, msg) { 132 | if( !angular.isObject(ref) || 133 | typeof(ref.ref) !== 'function' || 134 | typeof(ref.ref().transaction) !== 'function' ) { 135 | throw new Error(msg || 'Invalid Wilddog reference'); 136 | } 137 | }, 138 | 139 | // http://stackoverflow.com/questions/7509831/alternative-for-the-deprecated-proto 140 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create 141 | inherit: function(ChildClass, ParentClass, methods) { 142 | var childMethods = ChildClass.prototype; 143 | ChildClass.prototype = Object.create(ParentClass.prototype); 144 | ChildClass.prototype.constructor = ChildClass; // restoring proper constructor for child class 145 | angular.forEach(Object.keys(childMethods), function(k) { 146 | ChildClass.prototype[k] = childMethods[k]; 147 | }); 148 | if( angular.isObject(methods) ) { 149 | angular.extend(ChildClass.prototype, methods); 150 | } 151 | return ChildClass; 152 | }, 153 | 154 | getPrototypeMethods: function(inst, iterator, context) { 155 | var methods = {}; 156 | var objProto = Object.getPrototypeOf({}); 157 | var proto = angular.isFunction(inst) && angular.isObject(inst.prototype)? 158 | inst.prototype : Object.getPrototypeOf(inst); 159 | while(proto && proto !== objProto) { 160 | for (var key in proto) { 161 | // we only invoke each key once; if a super is overridden it's skipped here 162 | if (proto.hasOwnProperty(key) && !methods.hasOwnProperty(key)) { 163 | methods[key] = true; 164 | iterator.call(context, proto[key], key, proto); 165 | } 166 | } 167 | proto = Object.getPrototypeOf(proto); 168 | } 169 | }, 170 | 171 | getPublicMethods: function(inst, iterator, context) { 172 | utils.getPrototypeMethods(inst, function(m, k) { 173 | if( typeof(m) === 'function' && k.charAt(0) !== '_' ) { 174 | iterator.call(context, m, k); 175 | } 176 | }); 177 | }, 178 | 179 | defer: $q.defer, 180 | 181 | reject: $q.reject, 182 | 183 | resolve: $q.when, 184 | 185 | //TODO: Remove false branch and use only angular implementation when we drop angular 1.2.x support. 186 | promise: angular.isFunction($q) ? $q : Q, 187 | 188 | makeNodeResolver:function(deferred){ 189 | return function(err,result){ 190 | if(err === null){ 191 | if(arguments.length > 2){ 192 | result = Array.prototype.slice.call(arguments,1); 193 | } 194 | deferred.resolve(result); 195 | } 196 | else { 197 | deferred.reject(err); 198 | } 199 | }; 200 | }, 201 | 202 | wait: function(fn, wait) { 203 | var to = $timeout(fn, wait||0); 204 | return function() { 205 | if( to ) { 206 | $timeout.cancel(to); 207 | to = null; 208 | } 209 | }; 210 | }, 211 | 212 | compile: function(fn) { 213 | return $rootScope.$evalAsync(fn||function() {}); 214 | }, 215 | 216 | deepCopy: function(obj) { 217 | if( !angular.isObject(obj) ) { return obj; } 218 | var newCopy = angular.isArray(obj) ? obj.slice() : angular.extend({}, obj); 219 | for (var key in newCopy) { 220 | if (newCopy.hasOwnProperty(key)) { 221 | if (angular.isObject(newCopy[key])) { 222 | newCopy[key] = utils.deepCopy(newCopy[key]); 223 | } 224 | } 225 | } 226 | return newCopy; 227 | }, 228 | 229 | trimKeys: function(dest, source) { 230 | utils.each(dest, function(v,k) { 231 | if( !source.hasOwnProperty(k) ) { 232 | delete dest[k]; 233 | } 234 | }); 235 | }, 236 | 237 | scopeData: function(dataOrRec) { 238 | var data = { 239 | $id: dataOrRec.$id, 240 | $priority: dataOrRec.$priority 241 | }; 242 | var hasPublicProp = false; 243 | utils.each(dataOrRec, function(v,k) { 244 | hasPublicProp = true; 245 | data[k] = utils.deepCopy(v); 246 | }); 247 | if(!hasPublicProp && dataOrRec.hasOwnProperty('$value')){ 248 | data.$value = dataOrRec.$value; 249 | } 250 | return data; 251 | }, 252 | 253 | updateRec: function(rec, snap) { 254 | var data = snap.val(); 255 | var oldData = angular.extend({}, rec); 256 | 257 | // deal with primitives 258 | if( !angular.isObject(data) ) { 259 | rec.$value = data; 260 | data = {}; 261 | } 262 | else { 263 | delete rec.$value; 264 | } 265 | 266 | // apply changes: remove old keys, insert new data, set priority 267 | utils.trimKeys(rec, data); 268 | angular.extend(rec, data); 269 | rec.$priority = snap.getPriority(); 270 | 271 | return !angular.equals(oldData, rec) || 272 | oldData.$value !== rec.$value || 273 | oldData.$priority !== rec.$priority; 274 | }, 275 | 276 | applyDefaults: function(rec, defaults) { 277 | if( angular.isObject(defaults) ) { 278 | angular.forEach(defaults, function(v,k) { 279 | if( !rec.hasOwnProperty(k) ) { 280 | rec[k] = v; 281 | } 282 | }); 283 | } 284 | return rec; 285 | }, 286 | 287 | dataKeys: function(obj) { 288 | var out = []; 289 | utils.each(obj, function(v,k) { 290 | out.push(k); 291 | }); 292 | return out; 293 | }, 294 | 295 | each: function(obj, iterator, context) { 296 | if(angular.isObject(obj)) { 297 | for (var k in obj) { 298 | if (obj.hasOwnProperty(k)) { 299 | var c = k.charAt(0); 300 | if( c !== '_' && c !== '$' && c !== '.' ) { 301 | iterator.call(context, obj[k], k, obj); 302 | } 303 | } 304 | } 305 | } 306 | else if(angular.isArray(obj)) { 307 | for(var i = 0, len = obj.length; i < len; i++) { 308 | iterator.call(context, obj[i], i, obj); 309 | } 310 | } 311 | return obj; 312 | }, 313 | 314 | /** 315 | * A utility for retrieving a Wilddog reference or DataSnapshot's 316 | * key name. This is backwards-compatible with `name()` from Wilddog 317 | * 1.x.x and `key()` from Wilddog 2.0.0+. Once support for Wilddog 318 | * 1.x.x is dropped in wild-angular, this helper can be removed. 319 | */ 320 | getKey: function(refOrSnapshot) { 321 | return (typeof refOrSnapshot.key === 'function') ? refOrSnapshot.key() : refOrSnapshot.name(); 322 | }, 323 | 324 | /** 325 | * A utility for converting records to JSON objects 326 | * which we can save into Wilddog. It asserts valid 327 | * keys and strips off any items prefixed with $. 328 | * 329 | * If the rec passed into this method has a toJSON() 330 | * method, that will be used in place of the custom 331 | * functionality here. 332 | * 333 | * @param rec 334 | * @returns {*} 335 | */ 336 | toJSON: function(rec) { 337 | var dat; 338 | if( !angular.isObject(rec) ) { 339 | rec = {$value: rec}; 340 | } 341 | if (angular.isFunction(rec.toJSON)) { 342 | dat = rec.toJSON(); 343 | } 344 | else { 345 | dat = {}; 346 | utils.each(rec, function (v, k) { 347 | dat[k] = stripDollarPrefixedKeys(v); 348 | }); 349 | } 350 | if( angular.isDefined(rec.$value) && Object.keys(dat).length === 0 && rec.$value !== null ) { 351 | dat['.value'] = rec.$value; 352 | } 353 | if( angular.isDefined(rec.$priority) && Object.keys(dat).length > 0 && rec.$priority !== null ) { 354 | dat['.priority'] = rec.$priority; 355 | } 356 | angular.forEach(dat, function(v,k) { 357 | if (k.match(/[.$\[\]#\/]/) && k !== '.value' && k !== '.priority' ) { 358 | throw new Error('Invalid key ' + k + ' (cannot contain .$[]#)'); 359 | } 360 | else if( angular.isUndefined(v) ) { 361 | throw new Error('Key '+k+' was undefined. Cannot pass undefined in JSON. Use null instead.'); 362 | } 363 | }); 364 | return dat; 365 | }, 366 | 367 | doSet: function(ref, data) { 368 | var def = utils.defer(); 369 | if( angular.isFunction(ref.set) || !angular.isObject(data) ) { 370 | // this is not a query, just do a flat set 371 | ref.set(data, utils.makeNodeResolver(def)); 372 | } 373 | else { 374 | var dataCopy = angular.extend({}, data); 375 | // this is a query, so we will replace all the elements 376 | // of this query with the value provided, but not blow away 377 | // the entire Wilddog path 378 | ref.once('value', function(snap) { 379 | snap.forEach(function(ss) { 380 | if( !dataCopy.hasOwnProperty(utils.getKey(ss)) ) { 381 | dataCopy[utils.getKey(ss)] = null; 382 | } 383 | }); 384 | ref.ref().update(dataCopy, utils.makeNodeResolver(def)); 385 | }, function(err) { 386 | def.reject(err); 387 | }); 388 | } 389 | return def.promise; 390 | }, 391 | 392 | doRemove: function(ref) { 393 | var def = utils.defer(); 394 | if( angular.isFunction(ref.remove) ) { 395 | // ref is not a query, just do a flat remove 396 | ref.remove(utils.makeNodeResolver(def)); 397 | } 398 | else { 399 | // ref is a query so let's only remove the 400 | // items in the query and not the entire path 401 | ref.once('value', function(snap) { 402 | var promises = []; 403 | snap.forEach(function(ss) { 404 | var d = utils.defer(); 405 | promises.push(d.promise); 406 | ss.ref().remove(utils.makeNodeResolver(def)); 407 | }); 408 | utils.allPromises(promises) 409 | .then(function() { 410 | def.resolve(ref); 411 | }, 412 | function(err){ 413 | def.reject(err); 414 | } 415 | ); 416 | }, function(err) { 417 | def.reject(err); 418 | }); 419 | } 420 | return def.promise; 421 | }, 422 | 423 | /** 424 | * wild-angular version number. 425 | */ 426 | VERSION: '0.0.0', 427 | 428 | allPromises: $q.all.bind($q) 429 | }; 430 | 431 | return utils; 432 | } 433 | ]); 434 | 435 | function stripDollarPrefixedKeys(data) { 436 | if( !angular.isObject(data) ) { return data; } 437 | var out = angular.isArray(data)? [] : {}; 438 | angular.forEach(data, function(v,k) { 439 | if(typeof k !== 'string' || k.charAt(0) !== '$') { 440 | out[k] = stripDollarPrefixedKeys(v); 441 | } 442 | }); 443 | return out; 444 | } 445 | })(); 446 | -------------------------------------------------------------------------------- /src/wilddog.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module("wilddog") 5 | 6 | /** @deprecated */ 7 | .factory("$wilddog", function() { 8 | return function() { 9 | throw new Error('$wilddog has been removed. You may instantiate $wilddogArray and $wilddogObject ' + 10 | 'directly now. For simple write operations, just use the Wilddog ref directly. ' + 11 | 'See the wild-angular 1.0.0 changelog for details: https://www.wilddog.com/docs/web/libraries/angular/changelog.html'); 12 | }; 13 | }); 14 | 15 | })(); 16 | -------------------------------------------------------------------------------- /tests/automatic_karma.conf.js: -------------------------------------------------------------------------------- 1 | // Configuration file for Karma 2 | // http://karma-runner.github.io/0.10/config/configuration-file.html 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | frameworks: ['jasmine'], 7 | browsers: ['PhantomJS'], 8 | reporters: ['spec', 'failed', 'coverage'], 9 | autowatch: false, 10 | singleRun: true, 11 | 12 | preprocessors: { 13 | "../src/*.js": "coverage", 14 | "./fixtures/**/*.json": "html2js" 15 | }, 16 | 17 | coverageReporter: { 18 | reporters: [ 19 | { 20 | // Nice HTML reports on developer machines, but not on Travis 21 | type: process.env.TRAVIS ? "lcovonly" : "lcov", 22 | dir: "coverage", 23 | subdir: "." 24 | }, 25 | { 26 | type: "text-summary" 27 | } 28 | ] 29 | }, 30 | 31 | files: [ 32 | '../node_modules/angular/angular.js', 33 | '../node_modules/angular-mocks/angular-mocks.js', 34 | '../node_modules/wilddog/lib/wilddog-web.js', 35 | 'lib/**/*.js', 36 | '../src/module.js', 37 | '../src/**/*.js', 38 | 'mocks/**/*.js', 39 | "fixtures/**/*.json", 40 | 'unit/**/*.spec.js' 41 | ] 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /tests/browsers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "chrome", 4 | "version": "35", 5 | "platform": "OS X 10.9" 6 | }, 7 | { 8 | "name": "firefox", 9 | "version": "30" 10 | }, 11 | { 12 | "name": "safari", 13 | "platform": "OS X 10.9", 14 | "version": "7" 15 | }, 16 | { 17 | "device": "iPhone", 18 | "name": "iphone", 19 | "platform": "OS X 10.9", 20 | "version": "7.1" 21 | }, 22 | { 23 | "device": "Android", 24 | "name": "android", 25 | "platform": "linux", 26 | "version": "4.3" 27 | }, 28 | { 29 | "name": "internet explorer", 30 | "platform": "Windows 8.1", 31 | "version": "11" 32 | }, 33 | { 34 | "name": "internet explorer", 35 | "platform": "Windows 7", 36 | "version": "9" 37 | } 38 | ] 39 | -------------------------------------------------------------------------------- /tests/fixtures/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "a": { 4 | "aString": "alpha", 5 | "aNumber": 1, 6 | "aBoolean": false 7 | }, 8 | "b": { 9 | "aString": "bravo", 10 | "aNumber": 2, 11 | "aBoolean": true 12 | }, 13 | "c": { 14 | "aString": "charlie", 15 | "aNumber": 3, 16 | "aBoolean": true 17 | }, 18 | "d": { 19 | "aString": "delta", 20 | "aNumber": 4, 21 | "aBoolean": true 22 | }, 23 | "e": { 24 | "aString": "echo", 25 | "aNumber": 5 26 | } 27 | }, 28 | "index": { 29 | "b": true, 30 | "c": 1, 31 | "e": false, 32 | "z": true 33 | }, 34 | "ordered": { 35 | "null_a": { 36 | "aNumber": 0, 37 | "aLetter": "a" 38 | }, 39 | "null_b": { 40 | "aNumber": 0, 41 | "aLetter": "b" 42 | }, 43 | "null_c": { 44 | "aNumber": 0, 45 | "aLetter": "c" 46 | }, 47 | "num_1_a": { 48 | ".priority": 1, 49 | "aNumber": 1 50 | }, 51 | "num_1_b": { 52 | ".priority": 1, 53 | "aNumber": 1 54 | }, 55 | "num_2": { 56 | ".priority": 2, 57 | "aNumber": 2 58 | }, 59 | "num_3": { 60 | ".priority": 3, 61 | "aNumber": 3 62 | }, 63 | "char_a_1": { 64 | ".priority": "a", 65 | "aNumber": 1, 66 | "aLetter": "a" 67 | }, 68 | "char_a_2": { 69 | ".priority": "a", 70 | "aNumber": 2, 71 | "aLetter": "a" 72 | }, 73 | "char_b": { 74 | ".priority": "b", 75 | "aLetter": "b" 76 | }, 77 | "char_c": { 78 | ".priority": "c", 79 | "aLetter": "c" 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tests/lib/jasmineMatchers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Adds matchers to Jasmine so they can be called from test units 3 | * These are handy for debugging because they produce better error 4 | * messages than "Expected false to be true" 5 | */ 6 | beforeEach(function() { 7 | 'use strict'; 8 | 9 | function extendedTypeOf(x) { 10 | var actual; 11 | if( isArray(x) ) { 12 | actual = 'array'; 13 | } 14 | else if( x === null ) { 15 | actual = 'null'; 16 | } 17 | else { 18 | actual = typeof x; 19 | } 20 | return actual.toLowerCase(); 21 | } 22 | 23 | jasmine.addMatchers({ 24 | toBeAWilddogRef: function() { 25 | return { 26 | compare: function(actual) { 27 | var type = extendedTypeOf(actual); 28 | var pass = isWilddogRef(actual); 29 | var notText = pass? ' not' : ''; 30 | var msg = 'Expected ' + type + notText + ' to be a Wilddog ref'; 31 | return {pass: pass, message: msg}; 32 | } 33 | } 34 | }, 35 | 36 | toBeASnapshot: function() { 37 | return { 38 | compare: function(actual) { 39 | var type = extendedTypeOf(actual); 40 | var pass = 41 | type === 'object' && 42 | typeof actual.val === 'function' && 43 | typeof actual.ref === 'function' && 44 | typeof actual.name === 'function'; 45 | var notText = pass? ' not' : ''; 46 | var msg = 'Expected ' + type + notText + ' to be a Wilddog snapshot'; 47 | return {pass: pass, message: msg}; 48 | } 49 | } 50 | }, 51 | 52 | toBeAPromise: function() { 53 | return { 54 | compare: function(obj) { 55 | var objType = extendedTypeOf(obj); 56 | var pass = 57 | objType === 'object' && 58 | typeof obj.then === 'function' && 59 | typeof obj.catch === 'function' && 60 | typeof obj.finally === 'function'; 61 | var notText = pass? ' not' : ''; 62 | var msg = 'Expected ' + objType + notText + ' to be a promise'; 63 | return {pass: pass, message: msg}; 64 | } 65 | } 66 | }, 67 | 68 | // inspired by: https://gist.github.com/prantlf/8631877 69 | toBeInstanceOf: function() { 70 | return { 71 | compare: function (actual, expected, name) { 72 | var result = { 73 | pass: actual instanceof expected 74 | }; 75 | var notText = result.pass? ' not' : ''; 76 | result.message = 'Expected ' + actual + notText + ' to be an instance of ' + (name||expected.constructor.name); 77 | return result; 78 | } 79 | }; 80 | }, 81 | 82 | /** 83 | * Checks type of a value. This method will also accept null and array 84 | * as valid types. It will not treat null or arrays as objects. Multiple 85 | * types can be passed into this method and it will be true if any matches 86 | * are found. 87 | */ 88 | toBeA: function() { 89 | return { 90 | compare: function() { 91 | var args = Array.prototype.slice.apply(arguments); 92 | return compare.apply(null, ['a'].concat(args)); 93 | } 94 | }; 95 | }, 96 | 97 | toBeAn: function() { 98 | return { 99 | compare: function(actual) { 100 | var args = Array.prototype.slice.apply(arguments); 101 | return compare.apply(null, ['an'].concat(args)); 102 | } 103 | } 104 | }, 105 | 106 | toHaveKey: function() { 107 | return { 108 | compare: function(actual, key) { 109 | var pass = 110 | actual && 111 | typeof(actual) === 'object' && 112 | actual.hasOwnProperty(key); 113 | var notText = pass? ' not' : ''; 114 | return { 115 | pass: pass, 116 | message: 'Expected key ' + key + notText + ' to exist in ' + extendedTypeOf(actual) 117 | } 118 | } 119 | } 120 | }, 121 | 122 | toHaveLength: function() { 123 | return { 124 | compare: function(actual, len) { 125 | var actLen = isArray(actual)? actual.length : 'not an array'; 126 | var pass = actLen === len; 127 | var notText = pass? ' not' : ''; 128 | return { 129 | pass: pass, 130 | message: 'Expected array ' + notText + ' to have length ' + len + ', but it was ' + actLen 131 | } 132 | } 133 | } 134 | }, 135 | 136 | toBeEmpty: function() { 137 | return { 138 | compare: function(actual) { 139 | var pass, contents; 140 | if( isObject(actual) ) { 141 | actual = Object.keys(actual); 142 | } 143 | if( isArray(actual) ) { 144 | pass = actual.length === 0; 145 | contents = 'had ' + actual.length + ' items'; 146 | } 147 | else { 148 | pass = false; 149 | contents = 'was not an array or object'; 150 | } 151 | var notText = pass? ' not' : ''; 152 | return { 153 | pass: pass, 154 | message: 'Expected collection ' + notText + ' to be empty, but it ' + contents 155 | } 156 | } 157 | } 158 | }, 159 | 160 | toHaveCallCount: function() { 161 | return { 162 | compare: function(spy, expCount) { 163 | var pass, not, count; 164 | count = spy.calls.count(); 165 | pass = count === expCount; 166 | not = pass? '" not' : '"'; 167 | return { 168 | pass: pass, 169 | message: 'Expected spy "' + spy.and.identity() + not + ' to have been called ' + expCount + ' times' 170 | + (pass? '' : ', but it was called ' + count) 171 | } 172 | } 173 | } 174 | } 175 | }); 176 | 177 | function isObject(x) { 178 | return x && typeof(x) === 'object' && !isArray(x); 179 | } 180 | 181 | function isArray(x) { 182 | if (typeof Array.isArray !== 'function') { 183 | return x && typeof x === 'object' && Object.prototype.toString.call(x) === '[object Array]'; 184 | } 185 | return Array.isArray(x); 186 | } 187 | 188 | function isWilddogRef(obj) { 189 | return extendedTypeOf(obj) === 'object' && 190 | typeof obj.ref === 'function' && 191 | typeof obj.set === 'function' && 192 | typeof obj.on === 'function' && 193 | typeof obj.once === 'function' && 194 | typeof obj.transaction === 'function'; 195 | } 196 | 197 | // inspired by: https://gist.github.com/prantlf/8631877 198 | function compare(article, actual) { 199 | var validTypes = Array.prototype.slice.call(arguments, 2); 200 | if( !validTypes.length ) { 201 | throw new Error('Must pass at least one valid type into toBeA() and toBeAn() functions'); 202 | } 203 | var verbiage = validTypes.length === 1 ? 'to be ' + article : 'to be one of'; 204 | var actualType = extendedTypeOf(actual); 205 | 206 | var found = false; 207 | for (var i = 0, len = validTypes.length; i < len; i++) { 208 | found = validTypes[i].toLowerCase() === actualType; 209 | if( found ) { break; } 210 | } 211 | 212 | var notText = found? ' not' : ''; 213 | var message = 'Expected ' + actualType + notText + ' ' + verbiage + ' ' + validTypes; 214 | 215 | return { pass: found, message: message }; 216 | } 217 | }); 218 | -------------------------------------------------------------------------------- /tests/lib/module.testutils.js: -------------------------------------------------------------------------------- 1 | angular.module('testutils', ['wilddog']) 2 | .factory('testutils', function($wilddogUtils) { 3 | var utils = { 4 | ref: function(key, base) { 5 | var ref = new MockWilddog().child(base||'data'); 6 | if( key ) { ref = ref.child(key); } 7 | return ref; 8 | }, 9 | 10 | deepCopyObject: function(obj) { 11 | var newCopy = angular.isArray(obj) ? obj.slice() : angular.extend({}, obj); 12 | for (var key in newCopy) { 13 | if (newCopy.hasOwnProperty(key)) { 14 | if (angular.isObject(newCopy[key])) { 15 | newCopy[key] = utils.deepCopyObject(newCopy[key]); 16 | } 17 | } 18 | } 19 | return newCopy; 20 | }, 21 | 22 | snap: function(data, refKey, pri) { 23 | return utils.refSnap(utils.ref(refKey), data, pri); 24 | }, 25 | 26 | refSnap: function(ref, data, pri) { 27 | data = copySnapData(data); 28 | return { 29 | ref: function () { 30 | return ref; 31 | }, 32 | val: function () { 33 | return data; 34 | }, 35 | getPriority: function () { 36 | return angular.isDefined(pri) ? pri : null; 37 | }, 38 | key: function() { 39 | return ref.ref().key(); 40 | }, 41 | name: function () { 42 | return ref.ref().key(); 43 | }, 44 | child: function (key) { 45 | var childData = angular.isObject(data) && data.hasOwnProperty(key) ? data[key] : null; 46 | return utils.fakeSnap(ref.child(key), childData, null); 47 | } 48 | } 49 | } 50 | }; 51 | 52 | function copySnapData(obj) { 53 | if (!angular.isObject(obj)) { 54 | return obj; 55 | } 56 | var copy = {}; 57 | $wilddogUtils.each(obj, function (v, k) { 58 | copy[k] = angular.isObject(v) ? utils.deepCopyObject(v) : v; 59 | }); 60 | return copy; 61 | } 62 | 63 | return utils; 64 | }); 65 | -------------------------------------------------------------------------------- /tests/local_protractor.conf.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | // Locally, we should just use the default standalone Selenium server 3 | // In Travis, we set up the Selenium serving via Sauce Labs 4 | 5 | // Tests to run 6 | specs: [ 7 | './protractor/**/*.spec.js' 8 | ], 9 | 10 | // Capabilities to be passed to the webdriver instance 11 | // For a full list of available capabilities, see https://code.google.com/p/selenium/wiki/DesiredCapabilities 12 | capabilities: { 13 | 'browserName': process.env.TRAVIS ? 'firefox' : 'chrome' 14 | }, 15 | 16 | // Calls to protractor.get() with relative paths will be prepended with the baseUrl 17 | baseUrl: 'http://localhost:3030/tests/protractor/', 18 | 19 | // Selector for the element housing the angular app 20 | rootElement: 'body', 21 | 22 | // Options to be passed to minijasminenode 23 | jasmineNodeOpts: { 24 | // onComplete will be called just before the driver quits. 25 | onComplete: null, 26 | // If true, display spec names. 27 | isVerbose: true, 28 | // If true, print colors to the terminal. 29 | showColors: true, 30 | // If true, include stack traces in failures. 31 | includeStackTrace: true, 32 | // Default time to wait in ms before a test fails. 33 | defaultTimeoutInterval: 20000 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /tests/manual/auth.spec.js: -------------------------------------------------------------------------------- 1 | angular.module('testx', ['wilddog']); 2 | 3 | describe("wild-angularAuth Test Suite", function() { 4 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000 5 | 6 | //constants 7 | var existingUser = { 8 | email: "user@domain.com", 9 | password: "aaaaaaaa" 10 | } 11 | var newUserInf = { 12 | email: "a" + Math.round(Math.random()*10000000000) + "@email.com", 13 | password: "Pw", 14 | newPW: "sdljf" 15 | }; 16 | 17 | //globals 18 | var $wilddog; 19 | var $wilddogSimpleLogin; 20 | var $timeout; 21 | var $rootScope; 22 | var ngFireRef; 23 | var ngSimpleLogin; 24 | 25 | function AsyncWaiter(done, events) { 26 | var eventsToComplete = events ? events : ["default"]; 27 | 28 | this.done = function(event) { 29 | var theEvent = event ? event : "default"; 30 | var ind = eventsToComplete.indexOf(theEvent); 31 | if(ind >= 0) { 32 | eventsToComplete.splice(ind, 1); 33 | } 34 | }; 35 | 36 | this.wait = function(message, timeout) { 37 | var to = setInterval(function() { 38 | try { 39 | //We have to call this because the angular $timeout service is mocked for these tests. 40 | $timeout.flush(); 41 | } catch(err) {} 42 | if( eventsToComplete.length === 0 ) { 43 | clearInterval(to); 44 | done(); 45 | } 46 | }, timeout ? timeout : 2000); 47 | } 48 | } 49 | 50 | beforeEach(function() { 51 | //do some initial setup 52 | if(ngFireRef == null) { 53 | module("testx") 54 | inject(function(_$wilddog_, _$wilddogSimpleLogin_, _$timeout_, _$rootScope_) { 55 | $wilddog = _$wilddog_; 56 | $wilddogSimpleLogin = _$wilddogSimpleLogin_; 57 | $timeout = _$timeout_; 58 | $rootScope = _$rootScope_; 59 | var ref = new Wilddog("https://angularfiretests.wilddogio.com"); 60 | ngFireRef = $wilddog(ref); 61 | ngSimpleLogin = $wilddogSimpleLogin(ref); 62 | 63 | //make sure we start logged-out. 64 | ngSimpleLogin.$logout(); 65 | }); 66 | } 67 | }); 68 | 69 | //We have this test first, to make sure that initial login state doesn't mess up the promise returned by 70 | //login. 71 | it("Email: failed login", function(done) { 72 | var waiter = new AsyncWaiter(done, ["future_failed", "error_event"]); 73 | 74 | var loginFuture = ngSimpleLogin.$login("password", { 75 | email: "someaccount@here.com", 76 | password: "sdkhfsdhkf" 77 | }); 78 | 79 | //make sure the future fails. 80 | loginFuture.then(function(user) { 81 | expect(true).toBe(false); // we should never get here. 82 | }, function(err) { 83 | expect(err).not.toBe(null); 84 | waiter.done("future_failed"); 85 | }) 86 | 87 | //make sure an error event is broadcast on rootScope 88 | var off = $rootScope.$on("$wilddogSimpleLogin:error", function(event, err) { 89 | expect(err).not.toBe(null); 90 | waiter.done("error_event"); 91 | off(); 92 | }); 93 | 94 | waiter.wait("email login failure"); 95 | }); 96 | 97 | //Ensure that getUserInfo gives us a null if we're logged out. 98 | it("getUserInfo() triggers promise and is initially null.", function(done) { 99 | var waiter = new AsyncWaiter(done); 100 | 101 | ngSimpleLogin.$getCurrentUser().then(function(info) { 102 | expect(info).toBe(null); 103 | waiter.done(); 104 | }); 105 | 106 | waiter.wait("get user info from promise"); 107 | }); 108 | 109 | //Make sure logins to providers we haven't enabled fail. 110 | it("Failed Facebook login", function(done) { 111 | var waiter = new AsyncWaiter(done, ["future_failed", "error_event"]); 112 | 113 | var loginFuture = ngSimpleLogin.$login("facebook"); 114 | 115 | //verify that the future throws an error 116 | loginFuture.then(function() { 117 | expect(true).toBe(false); // we should never get here. 118 | }, function(err) { 119 | expect(err).not.toBe(null); 120 | waiter.done("future_failed"); 121 | }) 122 | 123 | //verify that an error event is triggered on the root scope 124 | var off = $rootScope.$on("$wilddogSimpleLogin:error", function(event, err) { 125 | expect(err).not.toBe(null); 126 | waiter.done("error_event"); 127 | off(); 128 | }); 129 | 130 | waiter.wait("login to complete", 15000); 131 | }); 132 | 133 | //Login successfully to a twitter account 134 | it("Successful Twitter login", function(done) { 135 | var waiter = new AsyncWaiter(done, ["user_info", "login_event"]); 136 | 137 | var loginFuture = ngSimpleLogin.$login("twitter"); 138 | 139 | //verify that the future throws an error 140 | loginFuture.then(function(user) { 141 | expect(user).not.toBe(null); 142 | waiter.done("user_info"); 143 | }, function(err) { 144 | //die 145 | expect(true).toBe(false); 146 | }); 147 | 148 | //verify that a login event is triggered on the root scope. Wrap it so that we don't see events for initial state. 149 | ngSimpleLogin.$getCurrentUser().then(function() { 150 | var off = $rootScope.$on("$wilddogSimpleLogin:login", function(event, user) { 151 | expect(user).not.toBe(null); 152 | waiter.done("login_event"); 153 | off(); 154 | }); 155 | }); 156 | 157 | waiter.wait("login failure to occur", 15000); 158 | }); 159 | 160 | //Check that email login works 161 | it("Email: login", function(done) { 162 | var waiter = new AsyncWaiter(done, ["future_success", "login_event"]); 163 | 164 | var loginFuture = ngSimpleLogin.$login("password", existingUser); 165 | 166 | //make sure the future succeeds. 167 | loginFuture.then(function(user) { 168 | expect(user.email).toBe(existingUser.email); 169 | waiter.done("future_success"); 170 | }, function(err) { 171 | expect(false).toBe(true); //die 172 | }) 173 | 174 | //make sure an error event is broadcast on rootScope. Wrap it so that we don't see events for initial state. 175 | ngSimpleLogin.$getCurrentUser().then(function() { 176 | var off = $rootScope.$on("$wilddogSimpleLogin:login", function(event, user) { 177 | expect(user.email).toBe(existingUser.email); 178 | waiter.done("login_event"); 179 | off(); 180 | 181 | //now check that the user model has actually been updated 182 | expect(ngSimpleLogin.user.email).toBe(existingUser.email); 183 | }); 184 | }); 185 | 186 | waiter.wait("email login success"); 187 | }); 188 | 189 | it("getCurrentUser for logged-in state", function(done) { 190 | var waiter = new AsyncWaiter(done); 191 | 192 | var promise = ngSimpleLogin.$getCurrentUser(); 193 | promise.then(function(user) { 194 | expect(user.email).toBe(existingUser.email); 195 | waiter.done(); 196 | }) 197 | 198 | waiter.wait("getting user info"); 199 | }); 200 | 201 | //Check to make sure logout works. 202 | it("Logout", function(done) { 203 | var waiter = new AsyncWaiter(done, ["future", "event"]); 204 | 205 | ngSimpleLogin.$logout(); 206 | 207 | //Verify that the user is immediately logged out. 208 | var future = ngSimpleLogin.$getCurrentUser(); 209 | future.then(function(user) { 210 | expect(user).toBe(null); 211 | waiter.done("future"); 212 | }) 213 | 214 | //verify that a logout event is triggered on the root scope 215 | var off = $rootScope.$on("$wilddogSimpleLogin:logout", function(event) { 216 | waiter.done("event"); 217 | off(); 218 | }); 219 | 220 | waiter.wait("get user info after logout"); 221 | }); 222 | 223 | //Ensure we properly handle errors on account creation. 224 | it("Email: failed account creation", function(done) { 225 | var waiter = new AsyncWaiter(done, ["promise", "event"]); 226 | 227 | var promise = ngSimpleLogin.$createUser(existingUser.email, "xaaa"); 228 | promise.then(function(user) { 229 | expect(false).toBe(true); // die 230 | }, function(err) { 231 | expect(err.code).toBe('EMAIL_TAKEN'); 232 | waiter.done("promise"); 233 | }) 234 | 235 | var off = $rootScope.$on("$wilddogSimpleLogin:error", function(event, err) { 236 | expect(err).not.toBe(null); 237 | waiter.done("event"); 238 | off(); 239 | }); 240 | 241 | waiter.wait("failed account creation"); 242 | }); 243 | 244 | //Test account creation. 245 | it("Email: account creation", function(done) { 246 | var waiter = new AsyncWaiter(done, ["promise", "getuser"]); 247 | 248 | var accountEmail = "a" + Math.round(Math.random()*10000000000) + "@email.com"; 249 | 250 | var promise = ngSimpleLogin.$createUser(accountEmail, "aaa"); 251 | promise.then(function(user) { 252 | expect(user.email).toBe(accountEmail); 253 | waiter.done("promise"); 254 | }, function(err) { 255 | expect(false).toBe(true); //die 256 | }); 257 | 258 | //lets ensure we didn't get logged in. 259 | ngSimpleLogin.$getCurrentUser().then(function(user) { 260 | expect(user).toBe(null); 261 | waiter.done("getuser"); 262 | }); 263 | 264 | waiter.wait("account creation with noLogin", 1600); 265 | }); 266 | 267 | //Test logging into newly created user. 268 | it("Email: account creation with subsequent login", function(done) { 269 | var waiter = new AsyncWaiter(done, ["promise", "login"]); 270 | var promise = ngSimpleLogin.$createUser(newUserInf.email, newUserInf.password); 271 | promise.then(function(user) { 272 | expect(user.email).toBe(newUserInf.email); 273 | waiter.done("promise"); 274 | ngSimpleLogin.$login("password", newUserInf).then(function(user2) { 275 | expect(user2.email).toBe(newUserInf.email); 276 | waiter.done("login"); 277 | }, function(err) { 278 | expect(false).toBe(true); 279 | }); 280 | }, function(err) { 281 | expect(false).toBe(true); //die 282 | }); 283 | waiter.wait("account creation", 2000); 284 | }); 285 | 286 | 287 | it("Email: failed change password", function(done) { 288 | var waiter = new AsyncWaiter(done, ["promise", "event"]); 289 | 290 | var promise = ngSimpleLogin.$changePassword(existingUser.email, "pxz", "sdf"); 291 | promise.then(function() { 292 | expect(false).toBe(true); //die 293 | }, function(err) { 294 | expect(err).not.toBe(null); 295 | waiter.done("promise"); 296 | }) 297 | 298 | var off = $rootScope.$on("$wilddogSimpleLogin:error", function(event, err) { 299 | expect(err).not.toBe(null); 300 | waiter.done("event"); 301 | off(); 302 | }); 303 | 304 | waiter.wait("failed change password", 2000); 305 | }); 306 | 307 | it("Email: change password", function(done) { 308 | var waiter = new AsyncWaiter(done, ["fail", "succeed"]); 309 | 310 | //this should fail 311 | ngSimpleLogin.$changePassword(newUserInf.email, "88dfhjgerqwqq", newUserInf.newPW).then(function(user) { 312 | expect(true).toBe(false); //die 313 | }, function(err) { 314 | waiter.done("fail"); 315 | expect(err.code).toBe('INVALID_PASSWORD'); 316 | 317 | }); 318 | 319 | //this should succeed 320 | var promise = ngSimpleLogin.$changePassword(newUserInf.email, newUserInf.password, newUserInf.newPW); 321 | promise.then(function() { 322 | expect(true).toBe(true); 323 | waiter.done("succeed"); 324 | }, function(err) { 325 | expect(true).toBe(false); //die 326 | }); 327 | 328 | waiter.wait("change password", 2000); 329 | }); 330 | 331 | it("Email: remove user", function(done) { 332 | var waiter = new AsyncWaiter(done, ["fail", "success"]); 333 | 334 | ngSimpleLogin.$removeUser(newUserInf.email + "x", newUserInf.newPW).then(function() { 335 | expect(true).toBe(false); //die 336 | }, function(err) { 337 | //this one doesn't exist, so it should fail 338 | expect(err).not.toBe(null); 339 | waiter.done("fail"); 340 | }); 341 | 342 | ngSimpleLogin.$removeUser(newUserInf.email, newUserInf.newPW).then(function() { 343 | waiter.done("success"); 344 | 345 | //TODO: this test should prob work, but we need to make Simple Login support this first 346 | //now make sure we've been logged out if we removed our own account 347 | //ngSimpleLogin.$getCurrentUser().then(function(user) { 348 | // expect(user).toBe(null); 349 | // waiter.done("check"); 350 | //}); 351 | }, function(err) { 352 | expect(true).toBe(false); //die 353 | }); 354 | 355 | waiter.wait("removeuser fail and success"); 356 | }); 357 | 358 | it("Email: reset password", function(done) { 359 | var waiter = new AsyncWaiter(done, ["fail", "success"]); 360 | 361 | ngSimpleLogin.$sendPasswordResetEmail("invalidemailaddress@example.org").then(function() { 362 | expect(true).toBe(false); 363 | }, function(err) { 364 | expect(err).not.toBe(null); 365 | waiter.done("fail"); 366 | }); 367 | 368 | ngSimpleLogin.$sendPasswordResetEmail("angularfiretests@mailinator.com").then(function() { 369 | waiter.done("success"); 370 | }, function(err) { 371 | expect(true).toBe(false); 372 | }); 373 | 374 | waiter.wait("resetpassword fail and success"); 375 | }); 376 | }); 377 | -------------------------------------------------------------------------------- /tests/manual_karma.conf.js: -------------------------------------------------------------------------------- 1 | // Configuration file for Karma 2 | // http://karma-runner.github.io/0.10/config/configuration-file.html 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | frameworks: ['jasmine'], 7 | browsers: ['Chrome'], 8 | reporters: ['spec', 'failed'], 9 | autowatch: false, 10 | singleRun: false, 11 | 12 | files: [ 13 | '../node_modules/angular/angular.js', 14 | '../node_modules/angular-mocks/angular-mocks.js', 15 | '../node_modules/wilddog/lib/wilddog-web.js', 16 | '../src/module.js', 17 | '../src/**/*.js', 18 | 'manual/**/*.spec.js' 19 | ] 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /tests/mocks/mocks.firebase.js: -------------------------------------------------------------------------------- 1 | MockWilddog.override(); 2 | -------------------------------------------------------------------------------- /tests/protractor/chat/chat.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WildDogTeam/lib-js-wildangular/1df47bed3a0ad9336c8e4778190652fe9d61b542/tests/protractor/chat/chat.css -------------------------------------------------------------------------------- /tests/protractor/chat/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | wild-angular Chat e2e Test 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |

22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
32 | {{ message.from }}: 33 | {{ message.content }} 34 |
35 |
36 | 37 | 38 |
39 | 40 | 41 |
42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /tests/protractor/chat/chat.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('chat', ['wilddog']); 2 | app.controller('ChatCtrl', function Chat($scope, $wilddogObject, $wilddogArray) { 3 | // Get a reference to the Wilddog 4 | var rootRef = new Wilddog('https://wilddog-angular.wilddogio.com'); 5 | 6 | // Store the data at a random push ID 7 | var chatRef = rootRef.child('chat').push(); 8 | 9 | // Put the random push ID into the DOM so that the test suite can grab it 10 | document.getElementById('pushId').innerHTML = chatRef.key(); 11 | 12 | var messagesRef = chatRef.child('messages').limitToLast(2); 13 | 14 | // Get the chat data as an object 15 | $scope.chat = $wilddogObject(chatRef); 16 | 17 | // Get the chat messages as an array 18 | $scope.messages = $wilddogArray(messagesRef); 19 | 20 | // Verify that $inst() works 21 | verify($scope.chat.$ref() === chatRef, 'Something is wrong with $wilddogObject.$ref().'); 22 | verify($scope.messages.$ref() === messagesRef, 'Something is wrong with $wilddogArray.$ref().'); 23 | 24 | // Initialize $scope variables 25 | $scope.message = ''; 26 | $scope.username = 'Guest' + Math.floor(Math.random() * 101); 27 | 28 | /* Clears the chat Wilddog reference */ 29 | $scope.clearRef = function () { 30 | chatRef.remove(); 31 | }; 32 | 33 | /* Adds a new message to the messages list and updates the messages count */ 34 | $scope.addMessage = function() { 35 | if ($scope.message !== '') { 36 | // Add a new message to the messages list 37 | $scope.messages.$add({ 38 | from: $scope.username, 39 | content: $scope.message 40 | }); 41 | 42 | // Reset the message input 43 | $scope.message = ''; 44 | } 45 | }; 46 | 47 | /* Destroys all wild-angular bindings */ 48 | $scope.destroy = function() { 49 | $scope.chat.$destroy(); 50 | $scope.messages.$destroy(); 51 | }; 52 | 53 | $scope.$on('destroy', function() { 54 | $scope.chat.$destroy(); 55 | $scope.messages.$destroy(); 56 | }); 57 | 58 | /* Logs a message and throws an error if the inputted expression is false */ 59 | function verify(expression, message) { 60 | if (!expression) { 61 | console.log(message); 62 | throw new Error(message); 63 | } 64 | } 65 | }); 66 | -------------------------------------------------------------------------------- /tests/protractor/chat/chat.spec.js: -------------------------------------------------------------------------------- 1 | var protractor = require('protractor'); 2 | var Wilddog = require('wilddog'); 3 | 4 | describe('Chat App', function () { 5 | // Reference to the Wilddog which stores the data for this demo 6 | var wilddogRef = new Wilddog('https://wild-angular.wilddogio.com/chat'); 7 | 8 | // Boolean used to load the page on the first test only 9 | var isPageLoaded = false; 10 | 11 | // Reference to the messages repeater 12 | var messages = element.all(by.repeater('message in messages')); 13 | 14 | var flow = protractor.promise.controlFlow(); 15 | 16 | function waitOne() { 17 | return protractor.promise.delayed(500); 18 | } 19 | 20 | function sleep() { 21 | flow.execute(waitOne); 22 | } 23 | 24 | function clearWilddogRef() { 25 | var deferred = protractor.promise.defer(); 26 | 27 | wilddogRef.remove(function(err) { 28 | if (err) { 29 | deferred.reject(err); 30 | } else { 31 | deferred.fulfill(); 32 | } 33 | }); 34 | 35 | return deferred.promise; 36 | } 37 | 38 | beforeEach(function (done) { 39 | if (!isPageLoaded) { 40 | isPageLoaded = true; 41 | 42 | // Navigate to the chat app 43 | browser.get('chat/chat.html').then(function() { 44 | // Get the random push ID where the data is being stored 45 | return $('#pushId').getText(); 46 | }).then(function(pushId) { 47 | // Update the Wilddog ref to point to the random push ID 48 | wilddogRef = wilddogRef.child(pushId); 49 | 50 | // Clear the Wilddog ref 51 | return clearWilddogRef(); 52 | }).then(done); 53 | } else { 54 | done(); 55 | } 56 | }); 57 | 58 | it('loads', function () { 59 | expect(browser.getTitle()).toEqual('wild-angular Chat e2e Test'); 60 | }); 61 | 62 | it('starts with an empty list of messages', function () { 63 | expect(messages.count()).toBe(0); 64 | }); 65 | 66 | it('adds new messages', function () { 67 | // Add three new messages by typing into the input and pressing enter 68 | var newMessageInput = element(by.model('message')); 69 | newMessageInput.sendKeys('Hey there!\n'); 70 | newMessageInput.sendKeys('Oh, hi. How are you?\n'); 71 | newMessageInput.sendKeys('Pretty fantastic!\n'); 72 | 73 | sleep(); 74 | 75 | // We should only have two messages in the repeater since we did a limit query 76 | expect(messages.count()).toBe(2); 77 | }); 78 | 79 | it('updates upon new remote messages', function () { 80 | flow.execute(function() { 81 | var def = protractor.promise.defer(); 82 | // Simulate a message being added remotely 83 | wilddogRef.child('messages').push({ 84 | from: 'Guest 2000', 85 | content: 'Remote message detected' 86 | }, function(err) { 87 | if( err ) { def.reject(err); } 88 | else { def.fulfill(); } 89 | }); 90 | return def.promise; 91 | }); 92 | 93 | // We should only have two messages in the repeater since we did a limit query 94 | expect(messages.count()).toBe(2); 95 | }); 96 | 97 | it('updates upon removed remote messages', function () { 98 | flow.execute(function() { 99 | var def = protractor.promise.defer(); 100 | // Simulate a message being deleted remotely 101 | var onCallback = wilddogRef.child('messages').limitToLast(1).on('child_added', function(childSnapshot) { 102 | wilddogRef.child('messages').off('child_added', onCallback); 103 | childSnapshot.ref().remove(function(err) { 104 | if( err ) { def.reject(err); } 105 | else { def.fulfill(); } 106 | }); 107 | }); 108 | return def.promise; 109 | }); 110 | 111 | // We should only have two messages in the repeater since we did a limit query 112 | expect(messages.count()).toBe(2); 113 | }); 114 | 115 | it('stops updating once the wild-angular bindings are destroyed', function () { 116 | // Destroy the wild-angular bindings 117 | $('#destroyButton').click(); 118 | 119 | sleep(); 120 | 121 | expect(messages.count()).toBe(0); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /tests/protractor/priority/priority.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WildDogTeam/lib-js-wildangular/1df47bed3a0ad9336c8e4778190652fe9d61b542/tests/protractor/priority/priority.css -------------------------------------------------------------------------------- /tests/protractor/priority/priority.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | wild-angular Priority e2e Test 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |

22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | 32 | 33 |
34 | 35 | 36 |
37 |
38 | {{ message.from }}: 39 | {{ message.content }} 40 | Priority: {{ message.$priority }} 41 |
42 |
43 | 44 | 45 |
46 | 47 | 48 |
49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /tests/protractor/priority/priority.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('priority', ['wilddog']); 2 | app.controller('PriorityCtrl', function Chat($scope, $wilddogArray, $wilddogObject) { 3 | // Get a reference to the Wilddog 4 | var messagesRef = new Wilddog('https://wild-angular.wilddogio.com/priority').push(); 5 | 6 | // Put the random push ID into the DOM so that the test suite can grab it 7 | document.getElementById('pushId').innerHTML = messagesRef.key(); 8 | 9 | // Get the chat messages as an array 10 | $scope.messages = $wilddogArray(messagesRef); 11 | 12 | // Verify that $inst() works 13 | verify($scope.messages.$ref() === messagesRef, 'Something is wrong with $wilddogArray.$ref().'); 14 | 15 | // Initialize $scope variables 16 | $scope.message = ''; 17 | $scope.username = 'Guest' + Math.floor(Math.random() * 101); 18 | 19 | /* Clears the priority Wilddog reference */ 20 | $scope.clearRef = function () { 21 | messagesRef.remove(); 22 | }; 23 | 24 | /* Adds a new message to the messages list */ 25 | $scope.addMessage = function () { 26 | if ($scope.message !== '') { 27 | // Add a new message to the messages list 28 | var priority = $scope.messages.length; 29 | $scope.messages.$add({ 30 | from: $scope.username, 31 | content: $scope.message 32 | }).then(function (ref) { 33 | var newItem = $wilddogObject(ref); 34 | 35 | newItem.$loaded().then(function (data) { 36 | verify(newItem === data, '$wilddogObject.$loaded() does not return correct value.'); 37 | 38 | // Update the message's priority 39 | newItem.$priority = priority; 40 | newItem.$save(); 41 | }); 42 | }, function (error) { 43 | verify(false, 'Something is wrong with $wilddogArray.$add().'); 44 | }); 45 | 46 | // Reset the message input 47 | $scope.message = ''; 48 | }; 49 | }; 50 | 51 | /* Destroys all wild-angular bindings */ 52 | $scope.destroy = function() { 53 | $scope.messages.$destroy(); 54 | }; 55 | 56 | /* Logs a message and throws an error if the inputted expression is false */ 57 | function verify(expression, message) { 58 | if (!expression) { 59 | console.log(message); 60 | throw new Error(message); 61 | } 62 | } 63 | }); 64 | -------------------------------------------------------------------------------- /tests/protractor/priority/priority.spec.js: -------------------------------------------------------------------------------- 1 | var protractor = require('protractor'); 2 | var Wilddog = require('wilddog'); 3 | 4 | describe('Priority App', function () { 5 | // Reference to the message repeater 6 | var messages = element.all(by.repeater('message in messages')); 7 | 8 | // Reference to the Wilddog which stores the data for this demo 9 | var wilddogRef = new Wilddog('https://wild-angular.wilddogio.com/priority'); 10 | 11 | // Boolean used to load the page on the first test only 12 | var isPageLoaded = false; 13 | 14 | var flow = protractor.promise.controlFlow(); 15 | 16 | function waitOne() { 17 | return protractor.promise.delayed(2000); 18 | } 19 | 20 | function sleep() { 21 | flow.execute(waitOne); 22 | } 23 | 24 | function clearWilddogRef() { 25 | var deferred = protractor.promise.defer(); 26 | 27 | wilddogRef.remove(function(err) { 28 | if (err) { 29 | deferred.reject(err); 30 | } else { 31 | deferred.fulfill(); 32 | } 33 | }); 34 | 35 | return deferred.promise; 36 | } 37 | 38 | beforeEach(function (done) { 39 | if (!isPageLoaded) { 40 | isPageLoaded = true; 41 | 42 | // Navigate to the priority app 43 | browser.get('priority/priority.html').then(function() { 44 | // Get the random push ID where the data is being stored 45 | return $('#pushId').getText(); 46 | }).then(function(pushId) { 47 | // Update the Wilddog ref to point to the random push ID 48 | wilddogRef = wilddogRef.child(pushId); 49 | 50 | // Clear the Wilddog ref 51 | return clearWilddogRef(); 52 | }).then(done); 53 | } else { 54 | done(); 55 | } 56 | }); 57 | 58 | 59 | it('loads', function () { 60 | expect(browser.getTitle()).toEqual('wild-angular Priority e2e Test'); 61 | }); 62 | 63 | it('starts with an empty list of messages', function () { 64 | // Make sure the page has no messages 65 | expect(messages.count()).toBe(0); 66 | }); 67 | 68 | it('adds new messages with the correct priority', function () { 69 | // Add three new messages by typing into the input and pressing enter 70 | var newMessageInput = element(by.model('message')); 71 | newMessageInput.sendKeys('Hey there!\n'); 72 | newMessageInput.sendKeys('Oh, hi. How are you?\n'); 73 | newMessageInput.sendKeys('Pretty fantastic!\n'); 74 | 75 | sleep(); 76 | 77 | // Make sure the page has three messages 78 | expect(messages.count()).toBe(3); 79 | 80 | // Make sure the priority of each message is correct 81 | expect($('.message:nth-of-type(1) .priority').getText()).toEqual('0'); 82 | expect($('.message:nth-of-type(2) .priority').getText()).toEqual('1'); 83 | expect($('.message:nth-of-type(3) .priority').getText()).toEqual('2'); 84 | 85 | // Make sure the content of each message is correct 86 | expect($('.message:nth-of-type(1) .content').getText()).toEqual('Hey there!'); 87 | expect($('.message:nth-of-type(2) .content').getText()).toEqual('Oh, hi. How are you?'); 88 | expect($('.message:nth-of-type(3) .content').getText()).toEqual('Pretty fantastic!'); 89 | }); 90 | /* 91 | it('responds to external priority updates', function () { 92 | flow.execute(moveRecords); 93 | flow.execute(waitOne); 94 | 95 | expect(messages.count()).toBe(3); 96 | expect($('.message:nth-of-type(1) .priority').getText()).toEqual('0'); 97 | expect($('.message:nth-of-type(2) .priority').getText()).toEqual('1'); 98 | expect($('.message:nth-of-type(3) .priority').getText()).toEqual('4'); 99 | 100 | // Make sure the content of each message is correct 101 | expect($('.message:nth-of-type(1) .content').getText()).toEqual('Pretty fantastic!'); 102 | expect($('.message:nth-of-type(2) .content').getText()).toEqual('Oh, hi. How are you?'); 103 | expect($('.message:nth-of-type(3) .content').getText()).toEqual('Hey there!'); 104 | 105 | function moveRecords() { 106 | return setPriority(null, 4) 107 | .then(setPriority.bind(null, 2, 0)); 108 | } 109 | 110 | function setPriority(start, pri) { 111 | var def = protractor.promise.defer(); 112 | wilddogRef.startAt(start).limitToFirst(1).once('child_added', function(snap) { 113 | var data = snap.val(); 114 | //todo https://github.com/wilddog/angularFire/issues/333 115 | //todo makeItChange just forces Angular to update the dom since it won't change 116 | //todo when a $ variable updates 117 | data.makeItChange = true; 118 | snap.ref().setWithPriority(data, pri, function(err) { 119 | if( err ) { def.reject(err); } 120 | else { def.fulfill(snap.key()); } 121 | }) 122 | }, def.reject); 123 | return def.promise; 124 | } 125 | }); 126 | */ 127 | }); 128 | -------------------------------------------------------------------------------- /tests/protractor/tictactoe/tictactoe.css: -------------------------------------------------------------------------------- 1 | .row { 2 | clear: both; 3 | } 4 | 5 | .cell { 6 | float: left; 7 | border: solid 2px black; 8 | height: 10px; 9 | width: 10px; 10 | padding: 40px; 11 | } -------------------------------------------------------------------------------- /tests/protractor/tictactoe/tictactoe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | wild-angular TicTacToe e2e Test 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |

22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
32 |
33 | {{ cell }} 34 |
35 |
36 |
37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /tests/protractor/tictactoe/tictactoe.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('tictactoe', ['wilddog']); 2 | app.controller('TicTacToeCtrl', function Chat($scope, $wilddogObject) { 3 | // Get a reference to the Wilddog 4 | var boardRef = new Wilddog('https://wild-angular.wilddogio.com/tictactoe'); 5 | 6 | // If the query string contains a push ID, use that as the child for data storage; 7 | // otherwise, generate a new random push ID 8 | var pushId; 9 | if (window.location && window.location.search) { 10 | pushId = window.location.search.substr(1).split('=')[1]; 11 | } 12 | if (pushId) { 13 | boardRef = boardRef.child(pushId); 14 | } else { 15 | boardRef = boardRef.push(); 16 | } 17 | 18 | // Put the random push ID into the DOM so that the test suite can grab it 19 | document.getElementById('pushId').innerHTML = boardRef.key(); 20 | 21 | // Get the board as an wild-angular object 22 | $scope.boardObject = $wilddogObject(boardRef); 23 | 24 | // Create a 3-way binding to Wilddog 25 | $scope.boardObject.$bindTo($scope, 'board'); 26 | 27 | // Verify that $inst() works 28 | verify($scope.boardObject.$ref() === boardRef, 'Something is wrong with $wilddogObject.$ref().'); 29 | 30 | // Initialize $scope variables 31 | $scope.whoseTurn = 'X'; 32 | 33 | /* Resets the tictactoe Wilddog reference */ 34 | $scope.resetRef = function () { 35 | ["x0", "x1", "x2"].forEach(function (xCoord) { 36 | $scope.board[xCoord] = { 37 | y0: "", 38 | y1: "", 39 | y2: "" 40 | }; 41 | }); 42 | }; 43 | 44 | 45 | /* Makes a move at the current cell */ 46 | $scope.makeMove = function(rowId, columnId) { 47 | // Only make a move if the current cell is not already taken 48 | if ($scope.board[rowId][columnId] === "") { 49 | // Update the board 50 | $scope.board[rowId][columnId] = $scope.whoseTurn; 51 | 52 | // Change whose turn it is 53 | $scope.whoseTurn = ($scope.whoseTurn === 'X') ? 'O' : 'X'; 54 | } 55 | }; 56 | 57 | /* Destroys all wild-angular bindings */ 58 | $scope.destroy = function() { 59 | $scope.boardObject.$destroy(); 60 | }; 61 | 62 | /* Logs a message and throws an error if the inputted expression is false */ 63 | function verify(expression, message) { 64 | if (!expression) { 65 | console.log(message); 66 | throw new Error(message); 67 | } 68 | } 69 | }); 70 | -------------------------------------------------------------------------------- /tests/protractor/tictactoe/tictactoe.spec.js: -------------------------------------------------------------------------------- 1 | var protractor = require('protractor'); 2 | var Wilddog = require('wilddog'); 3 | 4 | describe('TicTacToe App', function () { 5 | // Reference to the Wilddog which stores the data for this demo 6 | var wilddogRef = new Wilddog('https://wild-angular.wilddogio.com/tictactoe'); 7 | 8 | // Boolean used to load the page on the first test only 9 | var isPageLoaded = false; 10 | 11 | // Reference to the messages repeater 12 | //var cells = $$('.cell'); 13 | var cells = element.all(by.css('.cell')); 14 | 15 | var flow = protractor.promise.controlFlow(); 16 | 17 | function waitOne() { 18 | return protractor.promise.delayed(2000); 19 | } 20 | 21 | function sleep() { 22 | flow.execute(waitOne); 23 | } 24 | 25 | function clearWilddogRef() { 26 | var deferred = protractor.promise.defer(); 27 | 28 | wilddogRef.remove(function(err) { 29 | if (err) { 30 | deferred.reject(err); 31 | } else { 32 | deferred.fulfill(); 33 | } 34 | }); 35 | 36 | return deferred.promise; 37 | } 38 | 39 | beforeEach(function (done) { 40 | if (!isPageLoaded) { 41 | isPageLoaded = true; 42 | 43 | // Navigate to the tictactoe app 44 | browser.get('tictactoe/tictactoe.html').then(function() { 45 | // Get the random push ID where the data is being stored 46 | return $('#pushId').getText(); 47 | }).then(function(pushId) { 48 | // Update the Wilddog ref to point to the random push ID 49 | wilddogRef = wilddogRef.child(pushId); 50 | 51 | // Clear the Wilddog ref 52 | return clearWilddogRef(); 53 | }).then(done); 54 | } else { 55 | done(); 56 | } 57 | }); 58 | 59 | it('loads', function () { 60 | expect(browser.getTitle()).toEqual('wild-angular TicTacToe e2e Test'); 61 | }); 62 | 63 | it('starts with an empty board', function () { 64 | // Reset the board 65 | $('#resetRef').click(); 66 | 67 | // Wait for the board to reset 68 | sleep(); 69 | 70 | // Make sure the board has 9 cells 71 | var cells = element.all(by.css('.cell')); 72 | expect(cells.count()).toBe(9); 73 | 74 | // Make sure the board is empty 75 | cells.each(function(element) { 76 | expect(element.getText()).toBe(''); 77 | }); 78 | }); 79 | 80 | it('updates the board when cells are clicked', function () { 81 | // Make sure the board has 9 cells 82 | expect(cells.count()).toBe(9); 83 | 84 | // Make three moves by clicking the cells 85 | cells.get(0).click(); 86 | cells.get(2).click(); 87 | cells.get(6).click(); 88 | 89 | sleep(); 90 | 91 | // Make sure the content of each clicked cell is correct 92 | expect(cells.get(0).getText()).toBe('X'); 93 | expect(cells.get(2).getText()).toBe('O'); 94 | expect(cells.get(6).getText()).toBe('X'); 95 | }); 96 | 97 | it('persists state across refresh', function(done) { 98 | // Refresh the page, passing the push ID to use for data storage 99 | browser.get('tictactoe/tictactoe.html?pushId=' + wilddogRef.key()).then(function() { 100 | // Wait for wild-angular to sync the initial state 101 | sleep(); 102 | sleep(); 103 | 104 | // Make sure the board has 9 cells 105 | expect(cells.count()).toBe(9); 106 | 107 | // Make sure the content of each clicked cell is correct 108 | expect(cells.get(0).getText()).toBe('X'); 109 | expect(cells.get(2).getText()).toBe('O'); 110 | expect(cells.get(6).getText()).toBe('X'); 111 | 112 | done(); 113 | }); 114 | }); 115 | 116 | it('stops updating Wilddog once the wild-angular bindings are destroyed', function () { 117 | // Make sure the board has 9 cells 118 | expect(cells.count()).toBe(9); 119 | 120 | // Destroy the wild-angular bindings 121 | $('#destroyButton').click(); 122 | $('#resetRef').click(); 123 | 124 | // Click the middle cell 125 | cells.get(4).click(); 126 | 127 | sleep(); 128 | 129 | expect(cells.get(4).getText()).toBe('X'); 130 | 131 | sleep(); 132 | 133 | // make sure values are not changed on the server 134 | flow.execute(function() { 135 | var def = protractor.promise.defer(); 136 | wilddogRef.child('x1/y2').once('value', function (dataSnapshot) { 137 | expect(dataSnapshot.val()).toBe(''); 138 | def.fulfill(); 139 | }); 140 | return def.promise; 141 | }); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /tests/protractor/todo/todo.css: -------------------------------------------------------------------------------- 1 | #newTodoButton { 2 | width: 200px; 3 | } 4 | 5 | .edit { 6 | width: 250px; 7 | } -------------------------------------------------------------------------------- /tests/protractor/todo/todo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | wild-angular Todo e2e Test 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 |

23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | 34 | 35 |
36 | 37 |
38 | 39 | 40 |
41 |
42 | 43 | 44 | 45 |
46 |
47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /tests/protractor/todo/todo.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('todo', ['wilddog']); 2 | app. controller('TodoCtrl', function Todo($scope, $wilddogArray) { 3 | // Get a reference to the Wilddog 4 | var todosRef = new Wilddog('https://wild-angular.wilddogio.com/todo').push(); 5 | 6 | // Put the random push ID into the DOM so that the test suite can grab it 7 | document.getElementById('pushId').innerHTML = todosRef.key(); 8 | 9 | // Get the todos as an array 10 | $scope.todos = $wilddogArray(todosRef); 11 | 12 | // Verify that $ref() works 13 | verify($scope.todos.$ref() === todosRef, "Something is wrong with $wilddogArray.$ref()."); 14 | 15 | /* Clears the todos Wilddog reference */ 16 | $scope.clearRef = function () { 17 | todosRef.remove(); 18 | }; 19 | 20 | /* Adds a new todo item */ 21 | $scope.addTodo = function() { 22 | if ($scope.newTodo !== '') { 23 | $scope.todos.$add({ 24 | title: $scope.newTodo, 25 | completed: false 26 | }); 27 | 28 | $scope.newTodo = ''; 29 | } 30 | }; 31 | 32 | /* Adds a random todo item */ 33 | $scope.addRandomTodo = function () { 34 | $scope.newTodo = 'Todo ' + new Date().getTime(); 35 | $scope.addTodo(); 36 | }; 37 | 38 | /* Removes the todo item with the inputted ID */ 39 | $scope.removeTodo = function(id) { 40 | // Verify that $indexFor() and $keyAt() work 41 | verify($scope.todos.$indexFor($scope.todos.$keyAt(id)) === id, "Something is wrong with $wilddogArray.$indexFor() or WilddogArray.$keyAt()."); 42 | 43 | $scope.todos.$remove(id); 44 | }; 45 | 46 | /* Unbinds the todos array */ 47 | $scope.destroyArray = function() { 48 | $scope.todos.$destroy(); 49 | }; 50 | 51 | /* Logs a message and throws an error if the inputted expression is false */ 52 | function verify(expression, message) { 53 | if (!expression) { 54 | console.log(message); 55 | throw new Error(message); 56 | } 57 | } 58 | }); 59 | -------------------------------------------------------------------------------- /tests/protractor/todo/todo.spec.js: -------------------------------------------------------------------------------- 1 | var protractor = require('protractor'); 2 | var Wilddog = require('wilddog'); 3 | 4 | describe('Todo App', function () { 5 | // Reference to the Wilddog which stores the data for this demo 6 | var wilddogRef = new Wilddog('https://wild-angular.wilddogio.com/todo'); 7 | 8 | // Boolean used to load the page on the first test only 9 | var isPageLoaded = false; 10 | 11 | // Reference to the todos repeater 12 | var todos = element.all(by.repeater('(id, todo) in todos')); 13 | var flow = protractor.promise.controlFlow(); 14 | 15 | function waitOne() { 16 | return protractor.promise.delayed(500); 17 | } 18 | 19 | function sleep() { 20 | flow.execute(waitOne); 21 | } 22 | 23 | function clearWilddogRef() { 24 | var deferred = protractor.promise.defer(); 25 | 26 | wilddogRef.remove(function(err) { 27 | if (err) { 28 | deferred.reject(err); 29 | } else { 30 | deferred.fulfill(); 31 | } 32 | }); 33 | 34 | return deferred.promise; 35 | } 36 | 37 | beforeEach(function (done) { 38 | if (!isPageLoaded) { 39 | isPageLoaded = true; 40 | 41 | // Navigate to the todo app 42 | browser.get('todo/todo.html').then(function() { 43 | // Get the random push ID where the data is being stored 44 | return $('#pushId').getText(); 45 | }).then(function(pushId) { 46 | // Update the Wilddog ref to point to the random push ID 47 | wilddogRef = wilddogRef.child(pushId); 48 | 49 | // Clear the Wilddog ref 50 | return clearWilddogRef(); 51 | }).then(done); 52 | } else { 53 | done(); 54 | } 55 | }); 56 | 57 | it('loads', function () { 58 | expect(browser.getTitle()).toEqual('wild-angular Todo e2e Test'); 59 | }); 60 | 61 | it('starts with an empty list of Todos', function () { 62 | expect(todos.count()).toBe(0); 63 | }); 64 | 65 | it('adds new Todos', function () { 66 | // Add three new todos by typing into the input and pressing enter 67 | var newTodoInput = element(by.model('newTodo')); 68 | newTodoInput.sendKeys('Buy groceries\n'); 69 | newTodoInput.sendKeys('Run 10 miles\n'); 70 | newTodoInput.sendKeys('Build Wilddog\n'); 71 | 72 | sleep(); 73 | 74 | expect(todos.count()).toBe(3); 75 | }); 76 | 77 | it('adds random Todos', function () { 78 | // Add a three new random todos via the provided button 79 | var addRandomTodoButton = $('#addRandomTodoButton'); 80 | addRandomTodoButton.click(); 81 | addRandomTodoButton.click(); 82 | addRandomTodoButton.click(); 83 | 84 | sleep(); 85 | 86 | expect(todos.count()).toBe(6); 87 | }); 88 | 89 | it('removes Todos', function () { 90 | // Remove two of the todos via the provided buttons 91 | $('.todo:nth-of-type(2) .removeTodoButton').click(); 92 | $('.todo:nth-of-type(3) .removeTodoButton').click(); 93 | 94 | sleep(); 95 | 96 | expect(todos.count()).toBe(4); 97 | }); 98 | 99 | it('updates when a new Todo is added remotely', function () { 100 | // Simulate a todo being added remotely 101 | flow.execute(function() { 102 | var def = protractor.promise.defer(); 103 | wilddogRef.push({ 104 | title: 'Wash the dishes', 105 | completed: false 106 | }, function(err) { 107 | if( err ) { def.reject(err); } 108 | else { def.fulfill(); } 109 | }); 110 | return def.promise; 111 | }); 112 | expect(todos.count()).toBe(5); 113 | }); 114 | 115 | it('updates when an existing Todo is removed remotely', function () { 116 | // Simulate a todo being removed remotely 117 | flow.execute(function() { 118 | var def = protractor.promise.defer(); 119 | var onCallback = wilddogRef.limitToLast(1).on("child_added", function(childSnapshot) { 120 | // Make sure we only remove a child once 121 | wilddogRef.off("child_added", onCallback); 122 | 123 | childSnapshot.ref().remove(function(err) { 124 | if( err ) { def.reject(err); } 125 | else { def.fulfill(); } 126 | }); 127 | }); 128 | return def.promise; 129 | }); 130 | expect(todos.count()).toBe(4); 131 | }); 132 | 133 | it('stops updating once the sync array is destroyed', function () { 134 | // Destroy the sync array 135 | $('#destroyArrayButton').click(); 136 | 137 | sleep(); 138 | 139 | expect(todos.count()).toBe(0); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /tests/sauce_karma.conf.js: -------------------------------------------------------------------------------- 1 | // Configuration file for Karma 2 | // http://karma-runner.github.io/0.10/config/configuration-file.html 3 | 4 | module.exports = function(config) { 5 | var customLaunchers = require('./browsers.json') 6 | .reduce(function (browsers, browser) { 7 | browsers[(browser.name + '_v' + browser.version).replace(/(\.|\s)/g, '_')] = { 8 | base: 'SauceLabs', 9 | browserName: browser.name, 10 | platform: browser.platform, 11 | version: browser.version 12 | }; 13 | return browsers; 14 | }, {}); 15 | var browsers = Object.keys(customLaunchers); 16 | 17 | config.set({ 18 | basePath: '', 19 | frameworks: ['jasmine'], 20 | files: [ 21 | '../node_modules/angular/angular.js', 22 | '../node_modules/angular-mocks/angular-mocks.js', 23 | '../node_modules/wilddog/lib/wilddog-web.js', 24 | 'lib/**/*.js', 25 | '../dist/angularfire.js', 26 | 'mocks/**/*.js', 27 | 'unit/**/*.spec.js' 28 | ], 29 | 30 | logLevel: config.LOG_INFO, 31 | 32 | transports: ['xhr-polling'], 33 | 34 | sauceLabs: { 35 | testName: 'wild-angular Unit Tests', 36 | startConnect: false, 37 | tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER 38 | }, 39 | 40 | captureTimeout: 0, 41 | browserNoActivityTimeout: 120000, 42 | 43 | //Recommend starting Chrome manually with experimental javascript flag enabled, and open localhost:9876. 44 | customLaunchers: customLaunchers, 45 | browsers: browsers, 46 | reporters: ['dots', 'saucelabs'], 47 | singleRun: true 48 | 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /tests/sauce_protractor.conf.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | // Locally, we should just use the default standalone Selenium server 3 | // In Travis, we set up the Selenium serving via Sauce Labs 4 | sauceUser: process.env.SAUCE_USERNAME, 5 | sauceKey: process.env.SAUCE_ACCESS_KEY, 6 | 7 | // Tests to run 8 | specs: [ 9 | './protractor/**/*.spec.js' 10 | ], 11 | 12 | // Capabilities to be passed to the webdriver instance 13 | // For a full list of available capabilities, see https://code.google.com/p/selenium/wiki/DesiredCapabilities 14 | capabilities: { 15 | 'browserName': 'chrome', 16 | 'tunnel-identifier': process.env.TRAVIS_JOB_NUMBER, 17 | 'build': process.env.TRAVIS_BUILD_NUMBER, 18 | 'name': 'AngularFire Protractor Tests Build ' + process.env.TRAVIS_BUILD_NUMBER 19 | }, 20 | 21 | // Calls to protractor.get() with relative paths will be prepended with the baseUrl 22 | baseUrl: 'http://localhost:3030/tests/protractor/', 23 | 24 | // Selector for the element housing the angular app 25 | rootElement: 'body', 26 | 27 | // Options to be passed to Jasmine-node. 28 | onPrepare: function() { 29 | require('jasmine-spec-reporter'); 30 | // add jasmine spec reporter 31 | jasmine.getEnv().addReporter(new jasmine.SpecReporter({ 32 | displaySkippedSpec: true, 33 | displaySpecDuration: true 34 | })); 35 | }, 36 | 37 | // Options to be passed to minijasminenode 38 | jasmineNodeOpts: { 39 | // onComplete will be called just before the driver quits. 40 | onComplete: null, 41 | // If true, display spec names. 42 | isVerbose: true, 43 | // If true, print colors to the terminal. 44 | showColors: true, 45 | // If true, include stack traces in failures. 46 | includeStackTrace: true, 47 | // Default time to wait in ms before a test fails. 48 | defaultTimeoutInterval: 20000 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /tests/travis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | grunt build 4 | #grunt test:unit 5 | #grunt test:e2e 6 | #if [ $TRAVIS_TAG ]; then 7 | # grunt sauce:unit; 8 | #fi 9 | -------------------------------------------------------------------------------- /tests/unit/WilddogAuth.spec.js: -------------------------------------------------------------------------------- 1 | describe('WilddogAuth',function(){ 2 | 'use strict'; 3 | 4 | var $wilddogAuth, ref, authService, auth, result, failure, status, tick, $timeout, log, fakePromise, fakePromiseResolve, fakePromiseReject; 5 | 6 | beforeEach(function(){ 7 | 8 | log = { 9 | warn:[] 10 | }; 11 | // 12 | // module('wilddog.utils'); 13 | module('wilddog.auth',function($provide){ 14 | $provide.value('$log',{ 15 | warn:function(){ 16 | log.warn.push(Array.prototype.slice.call(arguments,0)); 17 | } 18 | }) 19 | }); 20 | module('testutils'); 21 | 22 | result = undefined; 23 | failure = undefined; 24 | status = null; 25 | 26 | fakePromise = function () { 27 | var resolve; 28 | var reject; 29 | var obj = { 30 | then: function (_resolve, _reject) { 31 | resolve = _resolve; 32 | reject = _reject; 33 | }, 34 | resolve: function (result) { 35 | resolve(result); 36 | }, 37 | reject: function (err) { 38 | reject(err); 39 | } 40 | }; 41 | fakePromiseReject = obj.reject; 42 | fakePromiseResolve = obj.resolve; 43 | return obj; 44 | } 45 | 46 | //offAuth, signInWithToken, updatePassword, changeEmail, removeUser 47 | auth = wilddog.auth(); 48 | ['signInWithCustomToken','signInAnonymously','signInWithEmailAndPassword', 49 | 'signInWithPhoneAndPassword','signInWithPopup','signInWithRedirect', 50 | 'signInWithCredential','signOut','createUserWithEmailAndPassword', 51 | 'createUserWithPhoneAndPassword','sendPasswordResetEmail','' 52 | ].forEach(function (funcName) { 53 | spyOn(auth, funcName).and.callFake(fakePromise); 54 | }); 55 | spyOn(auth, 'onAuthStateChanged').and.callFake(function (cb) { 56 | fakePromiseResolve = function (result) { 57 | cb(result); 58 | } 59 | return function () {/* Deregister */}; 60 | }); 61 | 62 | inject(function(_$wilddogAuth_,_$timeout_, $q, $rootScope){ 63 | $wilddogAuth = _$wilddogAuth_; 64 | authService = $wilddogAuth(auth); 65 | $timeout = _$timeout_; 66 | 67 | wilddog.sync.enableLogging(function () {tick()}); 68 | tick = function () { 69 | setTimeout(function() { 70 | $q.defer(); 71 | $rootScope.$digest(); 72 | try { 73 | $timeout.flush(); 74 | } catch (err) { 75 | // This throws an error when there is nothing to flush... 76 | } 77 | }) 78 | }; 79 | }); 80 | 81 | }); 82 | 83 | function wrapPromise(promise){ 84 | promise.then(function(_result_){ 85 | status = 'resolved'; 86 | result = _result_; 87 | },function(_failure_){ 88 | status = 'rejected'; 89 | failure = _failure_; 90 | }); 91 | } 92 | 93 | describe('Constructor', function() { 94 | it('will throw an error if a string is used in place of a Wilddog auth instance',function(){ 95 | expect(function(){ 96 | $wilddogAuth('https://some-wilddog.wilddogio.com/'); 97 | }).toThrow(); 98 | }); 99 | 100 | it('will throw an error if a sync instance is used in place of a Wilddog auth instance',function(){ 101 | expect(function(){ 102 | $wilddogAuth(wilddog.sync()); 103 | }).toThrow(); 104 | }); 105 | }); 106 | 107 | it('will throw an error if a sync reference is used in place of a Wilddog auth instance',function(){ 108 | expect(function(){ 109 | $wilddogAuth(wilddog.sync().ref()); 110 | }).toThrow(); 111 | }); 112 | 113 | it('will not throw an error if an auth instance is provided',function(){ 114 | $wilddogAuth(wilddog.auth()); 115 | }); 116 | 117 | describe('$signInWithCustomToken',function(){ 118 | it('should return a promise', function() { 119 | expect(authService.$signInWithCustomToken('myToken')).toBeAPromise(); 120 | }); 121 | 122 | it('passes custom token to underlying method',function(){ 123 | authService.$signInWithCustomToken('myToken'); 124 | expect(auth.signInWithCustomToken).toHaveBeenCalledWith('myToken'); 125 | }); 126 | 127 | it('will reject the promise if authentication fails',function(){ 128 | var promise = authService.$signInWithCustomToken('myToken'); 129 | wrapPromise(promise); 130 | fakePromiseReject('myError'); 131 | $timeout.flush(); 132 | expect(failure).toEqual('myError'); 133 | }); 134 | 135 | it('will resolve the promise upon authentication',function(){ 136 | var promise = authService.$signInWithCustomToken('myToken'); 137 | wrapPromise(promise); 138 | fakePromiseResolve('myResult'); 139 | $timeout.flush(); 140 | expect(result).toEqual('myResult'); 141 | }); 142 | }); 143 | 144 | describe('$signInAnonymously',function(){ 145 | it('should return a promise', function() { 146 | expect(authService.$signInAnonymously()).toBeAPromise(); 147 | }); 148 | 149 | it('passes options object to underlying method',function(){ 150 | authService.$signInAnonymously(); 151 | expect(auth.signInAnonymously).toHaveBeenCalled(); 152 | }); 153 | 154 | it('will reject the promise if authentication fails',function(){ 155 | var promise = authService.$signInAnonymously('myToken'); 156 | wrapPromise(promise); 157 | fakePromiseReject('myError'); 158 | $timeout.flush(); 159 | expect(failure).toEqual('myError'); 160 | }); 161 | 162 | it('will resolve the promise upon authentication',function(){ 163 | var promise = authService.$signInAnonymously('myToken'); 164 | wrapPromise(promise); 165 | fakePromiseResolve('myResult'); 166 | $timeout.flush(); 167 | expect(result).toEqual('myResult'); 168 | }); 169 | }); 170 | 171 | describe('$signInWithEmailWithPassword',function(){ 172 | it('should return a promise', function() { 173 | var email = 'abe@abe.abe'; 174 | var password = 'abeabeabe'; 175 | expect(authService.$signInWithEmailAndPassword(email, password)).toBeAPromise(); 176 | }); 177 | 178 | it('passes options and credentials object to underlying method',function(){ 179 | var email = 'abe@abe.abe'; 180 | var password = 'abeabeabe'; 181 | authService.$signInWithEmailAndPassword(email, password); 182 | expect(auth.signInWithEmailAndPassword).toHaveBeenCalledWith( 183 | email, password 184 | ); 185 | }); 186 | 187 | it('will reject the promise if authentication fails',function(){ 188 | var promise = authService.$signInWithEmailAndPassword('', ''); 189 | wrapPromise(promise); 190 | fakePromiseReject('myError'); 191 | $timeout.flush(); 192 | expect(failure).toEqual('myError'); 193 | }); 194 | 195 | it('will resolve the promise upon authentication',function(){ 196 | var promise = authService.$signInWithEmailAndPassword('', ''); 197 | wrapPromise(promise); 198 | fakePromiseResolve('myResult'); 199 | $timeout.flush(); 200 | expect(result).toEqual('myResult'); 201 | }); 202 | }); 203 | 204 | describe('$signInWithPopup',function(){ 205 | it('should return a promise', function() { 206 | var provider = new wilddog.auth.QQAuthProvider(); 207 | expect(authService.$signInWithPopup(provider)).toBeAPromise(); 208 | }); 209 | 210 | }); 211 | 212 | describe('$signInWithRedirect',function(){ 213 | it('should return a promise', function() { 214 | var provider = new wilddog.auth.QQAuthProvider(); 215 | expect(authService.$signInWithRedirect(provider)).toBeAPromise(); 216 | }); 217 | }); 218 | 219 | describe('$signInWithCredential',function(){ 220 | it('should return a promise', function() { 221 | expect(authService.$signInWithCredential('CREDENTIAL')).toBeAPromise(); 222 | }); 223 | 224 | it('passes credential object to underlying method',function(){ 225 | var credential = '!!!!'; 226 | authService.$signInWithCredential(credential); 227 | expect(auth.signInWithCredential).toHaveBeenCalledWith( 228 | credential 229 | ); 230 | }); 231 | 232 | it('will reject the promise if authentication fails',function(){ 233 | var promise = authService.$signInWithCredential('CREDENTIAL'); 234 | wrapPromise(promise); 235 | fakePromiseReject('myError'); 236 | $timeout.flush(); 237 | expect(failure).toEqual('myError'); 238 | }); 239 | 240 | it('will resolve the promise upon authentication',function(){ 241 | var promise = authService.$signInWithCredential('CREDENTIAL'); 242 | wrapPromise(promise); 243 | fakePromiseResolve('myResult'); 244 | $timeout.flush(); 245 | expect(result).toEqual('myResult'); 246 | }); 247 | }); 248 | 249 | describe('$getUser()',function(){ 250 | it('returns getUser() from backing auth instance',function(){ 251 | expect(authService.$getUser()).toEqual(auth.currentUser); 252 | }); 253 | }); 254 | 255 | describe('$signOut()',function(){ 256 | it('should return a promise', function() { 257 | expect(authService.$signOut()).toBeAPromise(); 258 | }); 259 | 260 | it('will call not signOut() on backing auth instance when user is not signed in',function(){ 261 | spyOn(authService._, 'getUser').and.callFake(function () { 262 | return null; 263 | }); 264 | authService.$signOut(); 265 | expect(auth.signOut).not.toHaveBeenCalled(); 266 | }); 267 | }); 268 | 269 | describe('$onAuthStateChanged()',function(){ 270 | it('calls onAuthStateChanged() on the backing auth instance', function() { 271 | function cb() {} 272 | var ctx = {}; 273 | authService.$onAuthStateChanged(cb, ctx); 274 | expect(auth.onAuthStateChanged).toHaveBeenCalledWith(jasmine.any(Function)); 275 | }); 276 | 277 | it('returns a deregistration function', function(){ 278 | var cb = function () {}; 279 | var ctx = {}; 280 | expect(authService.$onAuthStateChanged(cb, ctx)).toEqual(jasmine.any(Function)) 281 | }); 282 | }); 283 | 284 | describe('$requireUser()',function(){ 285 | it('will be resolved if user is logged in', function(done){ 286 | var credentials = {provider: 'qq'}; 287 | spyOn(authService._, 'getUser').and.callFake(function () { 288 | return credentials; 289 | }); 290 | 291 | authService.$requireUser() 292 | .then(function (result) { 293 | expect(result).toEqual(credentials); 294 | done(); 295 | }); 296 | 297 | fakePromiseResolve(credentials); 298 | tick(); 299 | }); 300 | 301 | it('will be rejected if user is not logged in', function(done){ 302 | spyOn(authService._, 'getUser').and.callFake(function () { 303 | return null; 304 | }); 305 | 306 | authService.$requireUser() 307 | .catch(function (error) { 308 | expect(error).toEqual('AUTH_REQUIRED'); 309 | done(); 310 | }); 311 | 312 | fakePromiseResolve(); 313 | tick(); 314 | }); 315 | }); 316 | 317 | describe('$requireUser(requireEmailVerification)',function(){ 318 | it('will be resolved if user is logged in and has a verified email address', function(done){ 319 | var credentials = {provider: 'qq'}; 320 | spyOn(authService._, 'getUser').and.callFake(function () { 321 | return credentials; 322 | }); 323 | 324 | authService.$requireUser(true) 325 | .then(function (result) { 326 | expect(result).toEqual(credentials); 327 | done(); 328 | }); 329 | 330 | fakePromiseResolve(credentials); 331 | tick(); 332 | }); 333 | 334 | it('will be resolved if user is logged in and we ignore email verification', function(done){ 335 | var credentials = {provider: 'qq'}; 336 | spyOn(authService._, 'getUser').and.callFake(function () { 337 | return credentials; 338 | }); 339 | 340 | authService.$requireUser(false) 341 | .then(function (result) { 342 | expect(result).toEqual(credentials); 343 | done(); 344 | }); 345 | 346 | fakePromiseResolve(credentials); 347 | tick(); 348 | }); 349 | 350 | it('will be rejected if user does not have a verified email address', function(done){ 351 | var credentials = {provider: 'qq'}; 352 | spyOn(authService._, 'getUser').and.callFake(function () { 353 | return credentials; 354 | }); 355 | 356 | authService.$requireUser(true) 357 | .catch(function (error) { 358 | expect(error).toEqual('EMAIL_VERIFICATION_REQUIRED'); 359 | done(); 360 | }); 361 | 362 | fakePromiseResolve(credentials); 363 | tick(); 364 | }); 365 | }); 366 | 367 | describe('$waitForSignIn()',function(){ 368 | it('will be resolved with authData if user is logged in', function(done){ 369 | var credentials = {provider: 'qq'}; 370 | spyOn(authService._, 'getUser').and.callFake(function () { 371 | return credentials; 372 | }); 373 | 374 | authService.$().then(function (result) { 375 | expect(result).toEqual(credentials); 376 | done(); 377 | }); 378 | 379 | fakePromiseResolve(credentials); 380 | tick(); 381 | }); 382 | 383 | it('will be resolved with null if user is not logged in', function(done){ 384 | spyOn(authService._, 'getUser').and.callFake(function () { 385 | return null; 386 | }); 387 | 388 | authService.$waitForSignIn().then(function (result) { 389 | expect(result).toEqual(null); 390 | done(); 391 | }); 392 | 393 | fakePromiseResolve(); 394 | tick(); 395 | }); 396 | }); 397 | 398 | describe('$createUserWithEmailAndPassword()',function(){ 399 | it('should return a promise', function() { 400 | var email = 'somebody@somewhere.com'; 401 | var password = '12345'; 402 | expect(authService.$createUserWithEmailAndPassword(email, password)).toBeAPromise(); 403 | }); 404 | 405 | it('passes email/password to method on backing ref',function(){ 406 | var email = 'somebody@somewhere.com'; 407 | var password = '12345'; 408 | authService.$createUserWithEmailAndPassword(email, password); 409 | expect(auth.createUserWithEmailAndPassword).toHaveBeenCalledWith( 410 | email, password); 411 | }); 412 | 413 | it('will reject the promise if creation fails',function(){ 414 | var promise = authService.$createUserWithEmailAndPassword('abe@abe.abe', '12345'); 415 | wrapPromise(promise); 416 | fakePromiseReject('myError'); 417 | $timeout.flush(); 418 | expect(failure).toEqual('myError'); 419 | }); 420 | 421 | it('will resolve the promise upon creation',function(){ 422 | var promise = authService.$createUserWithEmailAndPassword('abe@abe.abe', '12345'); 423 | wrapPromise(promise); 424 | fakePromiseResolve('myResult'); 425 | $timeout.flush(); 426 | expect(result).toEqual('myResult'); 427 | }); 428 | }); 429 | 430 | describe('$createUserWithPhoneAndPassword()',function(){ 431 | it('should return a promise', function() { 432 | var phone = '13288888888'; 433 | var password = '12345'; 434 | expect(authService.$createUserWithPhoneAndPassword(phone, password)).toBeAPromise(); 435 | }); 436 | 437 | it('passes email/password to method on backing ref',function(){ 438 | var phone = 'somebody@somewhere.com'; 439 | var password = '12345'; 440 | authService.$createUserWithPhoneAndPassword(phone, password); 441 | expect(auth.createUserWithEmailAndPassword).toHaveBeenCalledWith( 442 | phone, password); 443 | }); 444 | 445 | it('will reject the promise if creation fails',function(){ 446 | var promise = authService.$createUserWithPhoneAndPassword('13288888888', '12345'); 447 | wrapPromise(promise); 448 | fakePromiseReject('myError'); 449 | $timeout.flush(); 450 | expect(failure).toEqual('myError'); 451 | }); 452 | 453 | it('will resolve the promise upon creation',function(){ 454 | var promise = authService.$createUserWithPhoneAndPassword('13288888888', '12345'); 455 | wrapPromise(promise); 456 | fakePromiseResolve('myResult'); 457 | $timeout.flush(); 458 | expect(result).toEqual('myResult'); 459 | }); 460 | }); 461 | 462 | describe('$updatePassword()',function() { 463 | it('should return a promise', function() { 464 | var newPassword = 'CatInDatHat'; 465 | expect(authService.$updatePassword(newPassword)).toBeAPromise(); 466 | }); 467 | 468 | it('passes new password to method on backing auth instance',function(done) { 469 | spyOn(authService._, 'getUser').and.callFake(function () { 470 | return { 471 | updatePassword: function (password) { 472 | expect(password).toBe(newPassword); 473 | done(); 474 | } 475 | }; 476 | }); 477 | 478 | var newPassword = 'CatInDatHat'; 479 | authService.$updatePassword(newPassword); 480 | }); 481 | 482 | it('will reject the promise if creation fails',function(){ 483 | spyOn(authService._, 'getUser').and.callFake(function () { 484 | return { 485 | updatePassword: function (password) { 486 | return fakePromise(); 487 | } 488 | }; 489 | }); 490 | 491 | var promise = authService.$updatePassword('PASSWORD'); 492 | wrapPromise(promise); 493 | fakePromiseReject('myError'); 494 | $timeout.flush(); 495 | expect(failure).toEqual('myError'); 496 | }); 497 | 498 | it('will resolve the promise upon creation',function(){ 499 | spyOn(authService._, 'getUser').and.callFake(function () { 500 | return { 501 | updatePassword: function (password) { 502 | return fakePromise(); 503 | } 504 | }; 505 | }); 506 | 507 | var promise = authService.$updatePassword('PASSWORD'); 508 | wrapPromise(promise); 509 | fakePromiseResolve('myResult'); 510 | $timeout.flush(); 511 | expect(result).toEqual('myResult'); 512 | }); 513 | }); 514 | 515 | describe('$updateEmail()',function() { 516 | it('should return a promise', function() { 517 | var newEmail = 'abe@abe.abe'; 518 | expect(authService.$updateEmail(newEmail)).toBeAPromise(); 519 | }); 520 | 521 | it('passes new email to method on backing auth instance',function(done) { 522 | spyOn(authService._, 'getUser').and.callFake(function () { 523 | return { 524 | updateEmail: function (email) { 525 | expect(email).toBe(newEmail); 526 | done(); 527 | } 528 | }; 529 | }); 530 | 531 | var newEmail = 'abe@abe.abe'; 532 | authService.$updateEmail(newEmail); 533 | }); 534 | 535 | it('will reject the promise if creation fails',function(){ 536 | spyOn(authService._, 'getUser').and.callFake(function () { 537 | return { 538 | updateEmail: function (email) { 539 | return fakePromise(); 540 | } 541 | }; 542 | }); 543 | 544 | var promise = authService.$updateEmail('abe@abe.abe'); 545 | wrapPromise(promise); 546 | fakePromiseReject('myError'); 547 | $timeout.flush(); 548 | expect(failure).toEqual('myError'); 549 | }); 550 | 551 | it('will resolve the promise upon creation',function(){ 552 | spyOn(authService._, 'getUser').and.callFake(function () { 553 | return { 554 | updateEmail: function (email) { 555 | return fakePromise(); 556 | } 557 | }; 558 | }); 559 | 560 | var promise = authService.$updateEmail('abe@abe.abe'); 561 | wrapPromise(promise); 562 | fakePromiseResolve('myResult'); 563 | $timeout.flush(); 564 | expect(result).toEqual('myResult'); 565 | }); 566 | }); 567 | 568 | describe('$deleteUser()',function(){ 569 | it('should return a promise', function() { 570 | expect(authService.$deleteUser()).toBeAPromise(); 571 | }); 572 | 573 | it('calls delete on backing auth instance',function(done) { 574 | spyOn(authService._, 'getUser').and.callFake(function () { 575 | return { 576 | delete: function () { 577 | done(); 578 | } 579 | }; 580 | }); 581 | authService.$deleteUser(); 582 | }); 583 | 584 | it('will reject the promise if creation fails',function(){ 585 | spyOn(authService._, 'getUser').and.callFake(function () { 586 | return { 587 | delete: function () { 588 | return fakePromise(); 589 | } 590 | }; 591 | }); 592 | 593 | var promise = authService.$deleteUser(); 594 | wrapPromise(promise); 595 | fakePromiseReject('myError'); 596 | $timeout.flush(); 597 | expect(failure).toEqual('myError'); 598 | }); 599 | 600 | it('will resolve the promise upon creation',function(){ 601 | spyOn(authService._, 'getUser').and.callFake(function () { 602 | return { 603 | delete: function () { 604 | return fakePromise(); 605 | } 606 | }; 607 | }); 608 | 609 | var promise = authService.$deleteUser(); 610 | wrapPromise(promise); 611 | fakePromiseResolve('myResult'); 612 | $timeout.flush(); 613 | expect(result).toEqual('myResult'); 614 | }); 615 | }); 616 | 617 | describe('$sendPasswordResetEmail()',function(){ 618 | it('should return a promise', function() { 619 | var email = 'somebody@somewhere.com'; 620 | expect(authService.$sendPasswordResetEmail(email)).toBeAPromise(); 621 | }); 622 | 623 | it('passes email to method on backing auth instance',function(){ 624 | var email = 'somebody@somewhere.com'; 625 | authService.$sendPasswordResetEmail(email); 626 | expect(auth.sendPasswordResetEmail).toHaveBeenCalledWith(email); 627 | }); 628 | 629 | it('will reject the promise if creation fails',function(){ 630 | var promise = authService.$sendPasswordResetEmail('abe@abe.abe'); 631 | wrapPromise(promise); 632 | fakePromiseReject('myError'); 633 | $timeout.flush(); 634 | expect(failure).toEqual('myError'); 635 | }); 636 | 637 | it('will resolve the promise upon creation',function(){ 638 | var promise = authService.$sendPasswordResetEmail('abe@abe.abe'); 639 | wrapPromise(promise); 640 | fakePromiseResolve('myResult'); 641 | $timeout.flush(); 642 | expect(result).toEqual('myResult'); 643 | }); 644 | }); 645 | 646 | describe('$sendPasswordResetPhone()',function(){ 647 | it('should return a promise', function() { 648 | var phone = '13288888888'; 649 | expect(authService.$sendPasswordResetPhone(phone)).toBeAPromise(); 650 | }); 651 | 652 | it('passes email to method on backing auth instance',function(){ 653 | var phone = 'somebody@somewhere.com'; 654 | authService.$sendPasswordResetPhone(phone); 655 | expect(auth.sendPasswordResetEmail).toHaveBeenCalledWith(phone); 656 | }); 657 | 658 | it('will reject the promise if creation fails',function(){ 659 | var promise = authService.$sendPasswordResetPhone('13288888888'); 660 | wrapPromise(promise); 661 | fakePromiseReject('myError'); 662 | $timeout.flush(); 663 | expect(failure).toEqual('myError'); 664 | }); 665 | 666 | it('will resolve the promise upon creation',function(){ 667 | var promise = authService.$sendPasswordResetPhone('13288888888'); 668 | wrapPromise(promise); 669 | fakePromiseResolve('myResult'); 670 | $timeout.flush(); 671 | expect(result).toEqual('myResult'); 672 | }); 673 | }); 674 | }); 675 | -------------------------------------------------------------------------------- /tests/unit/utils.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | describe('$wilddogUtils', function () { 3 | var $utils, $timeout, testutils; 4 | 5 | var MOCK_DATA = { 6 | 'a': { 7 | aString: 'alpha', 8 | aNumber: 1, 9 | aBoolean: false 10 | }, 11 | 'b': { 12 | aString: 'bravo', 13 | aNumber: 2, 14 | aBoolean: true 15 | }, 16 | 'c': { 17 | aString: 'charlie', 18 | aNumber: 3, 19 | aBoolean: true 20 | }, 21 | 'd': { 22 | aString: 'delta', 23 | aNumber: 4, 24 | aBoolean: true 25 | }, 26 | 'e': { 27 | aString: 'echo', 28 | aNumber: 5 29 | } 30 | }; 31 | 32 | beforeEach(function () { 33 | module('wilddog'); 34 | module('testutils'); 35 | inject(function (_$wilddogUtils_, _$timeout_, _testutils_) { 36 | $utils = _$wilddogUtils_; 37 | $timeout = _$timeout_; 38 | testutils = _testutils_; 39 | }); 40 | }); 41 | 42 | describe('#batch', function() { 43 | it('should return a function', function() { 44 | expect(typeof $utils.batch()).toBe('function'); 45 | }); 46 | 47 | it('should trigger function with arguments', function() { 48 | var spy = jasmine.createSpy(); 49 | var b = $utils.batch(spy); 50 | b('foo', 'bar'); 51 | $timeout.flush(); 52 | expect(spy).toHaveBeenCalledWith('foo', 'bar'); 53 | }); 54 | 55 | it('should queue up requests until timeout', function() { 56 | var spy = jasmine.createSpy(); 57 | var b = $utils.batch(spy); 58 | for(var i=0; i < 4; i++) { 59 | b(i); 60 | } 61 | expect(spy).not.toHaveBeenCalled(); 62 | $timeout.flush(); 63 | expect(spy.calls.count()).toBe(4); 64 | }); 65 | 66 | it('should observe context', function() { 67 | var a = {}, b; 68 | var spy = jasmine.createSpy().and.callFake(function() { 69 | b = this; 70 | }); 71 | $utils.batch(spy, a)(); 72 | $timeout.flush(); 73 | expect(spy).toHaveBeenCalled(); 74 | expect(b).toBe(a); 75 | }); 76 | }); 77 | 78 | describe('#debounce', function(){ 79 | it('should trigger function with arguments',function(){ 80 | var spy = jasmine.createSpy(); 81 | $utils.debounce(spy,10)('foo', 'bar'); 82 | $timeout.flush(); 83 | expect(spy).toHaveBeenCalledWith('foo', 'bar'); 84 | }); 85 | 86 | it('should only trigger once, with most recent arguments',function(){ 87 | var spy = jasmine.createSpy(); 88 | var fn = $utils.debounce(spy,10); 89 | fn('foo', 'bar'); 90 | fn('baz', 'biz'); 91 | $timeout.flush(); 92 | expect(spy.calls.count()).toBe(1); 93 | expect(spy).toHaveBeenCalledWith('baz', 'biz'); 94 | }); 95 | 96 | it('should only trigger once (timing corner case)',function(){ 97 | var spy = jasmine.createSpy(); 98 | var fn = $utils.debounce(spy, null, 1, 2); 99 | fn('foo', 'bar'); 100 | var start = Date.now(); 101 | 102 | // block for 3ms without releasing 103 | while(Date.now() - start < 3){ } 104 | 105 | fn('bar', 'baz'); 106 | fn('baz', 'biz'); 107 | expect(spy).not.toHaveBeenCalled(); 108 | $timeout.flush(); 109 | expect(spy.calls.count()).toBe(1); 110 | expect(spy).toHaveBeenCalledWith('baz', 'biz'); 111 | }); 112 | }); 113 | 114 | describe('#updateRec', function() { 115 | it('should return true if changes applied', function() { 116 | var rec = {}; 117 | expect($utils.updateRec(rec, testutils.snap('foo'))).toBe(true); 118 | }); 119 | 120 | it('should be false if no changes applied', function() { 121 | var rec = {foo: 'bar', $id: 'foo', $priority: null}; 122 | expect($utils.updateRec(rec, testutils.snap({foo: 'bar'}, 'foo'))).toBe(false); 123 | }); 124 | 125 | it('should apply changes to record', function() { 126 | var rec = {foo: 'bar', bar: 'foo', $id: 'foo', $priority: null}; 127 | $utils.updateRec(rec, testutils.snap({bar: 'baz', baz: 'foo'})); 128 | expect(rec).toEqual({bar: 'baz', baz: 'foo', $id: 'foo', $priority: null}) 129 | }); 130 | 131 | it('should delete $value property if not a primitive',function(){ 132 | var rec = {$id:'foo', $priority:null, $value:null }; 133 | $utils.updateRec(rec, testutils.snap({bar: 'baz', baz:'foo'})); 134 | expect(rec).toEqual({bar: 'baz', baz: 'foo', $id: 'foo', $priority: null}); 135 | }); 136 | }); 137 | 138 | describe('#scopeData',function(){ 139 | it('$id, $priority, and $value are only private properties that get copied',function(){ 140 | var data = {$id:'foo',$priority:'bar',$value:null,$private1:'baz',$private2:'foo'}; 141 | expect($utils.scopeData(data)).toEqual({$id:'foo',$priority:'bar',$value:null}); 142 | }); 143 | 144 | it('all public properties will be copied',function(){ 145 | var data = {$id:'foo',$priority:'bar',public1:'baz',public2:'test'}; 146 | expect($utils.scopeData(data)).toEqual({$id:'foo',$priority:'bar',public1:'baz',public2:'test'}); 147 | }); 148 | 149 | it('$value will not be copied if public properties are present',function(){ 150 | var data = {$id:'foo',$priority:'bar',$value:'noCopy',public1:'baz',public2:'test'}; 151 | expect($utils.scopeData(data)).toEqual({$id:'foo',$priority:'bar',public1:'baz',public2:'test'}); 152 | }); 153 | }); 154 | 155 | describe('#applyDefaults', function() { 156 | it('should return rec', function() { 157 | var rec = {foo: 'bar'}; 158 | expect($utils.applyDefaults(rec), {bar: 'baz'}).toBe(rec); 159 | }); 160 | 161 | it('should do nothing if no defaults exist', function() { 162 | var rec = {foo: 'bar'}; 163 | $utils.applyDefaults(rec, null); 164 | expect(rec).toEqual({foo: 'bar'}); 165 | }); 166 | 167 | it('should add $$defaults if they exist', function() { 168 | var rec = {foo: 'foo', bar: 'bar', $id: 'foo', $priority: null}; 169 | var defaults = { baz: 'baz', bar: 'not_applied' }; 170 | $utils.applyDefaults(rec, defaults); 171 | expect(rec).toEqual({foo: 'foo', bar: 'bar', $id: 'foo', $priority: null, baz: 'baz'}); 172 | }); 173 | }); 174 | 175 | describe('#toJSON', function() { 176 | it('should use toJSON if it exists', function() { 177 | var json = {json: true}; 178 | var spy = jasmine.createSpy('toJSON').and.callFake(function() { 179 | return json; 180 | }); 181 | var F = function() {}; 182 | F.prototype.toJSON = spy; 183 | expect($utils.toJSON(new F())).toEqual(json); 184 | expect(spy).toHaveBeenCalled(); 185 | }); 186 | 187 | it('should use $value if found', function() { 188 | var json = {$value: 'foo'}; 189 | expect($utils.toJSON(json)).toEqual({'.value': json.$value}); 190 | }); 191 | 192 | it('should set $priority if exists', function() { 193 | var json = {$value: 'foo', $priority: 0}; 194 | expect($utils.toJSON(json)).toEqual({'.value': json.$value, '.priority': json.$priority}); 195 | }); 196 | 197 | it('should not set $priority if it is the only key', function() { 198 | var json = {$priority: true}; 199 | expect($utils.toJSON(json)).toEqual({}); 200 | }); 201 | 202 | it('should remove any variables prefixed with $', function() { 203 | var json = {foo: 'bar', $foo: '$bar'}; 204 | expect($utils.toJSON(json)).toEqual({foo: json.foo}); 205 | }); 206 | 207 | it('should remove any deeply nested variables prefixed with $', function() { 208 | var json = { 209 | arr: [[ 210 | {$$hashKey: false, $key: 1, alpha: 'alpha', bravo: {$$private: '$$private', $key: '$key', bravo: 'bravo'}}, 211 | {$$hashKey: false, $key: 1, alpha: 'alpha', bravo: {$$private: '$$private', $key: '$key', bravo: 'bravo'}} 212 | 213 | ], ["a", "b", {$$key: '$$key'}]], 214 | obj: { 215 | nest: {$$hashKey: false, $key: 1, alpha: 'alpha', bravo: {$$private: '$$private', $key: '$key', bravo: 'bravo'} } 216 | } 217 | }; 218 | 219 | expect($utils.toJSON(json)).toEqual({ 220 | arr: [[ 221 | {alpha: 'alpha', bravo: {bravo: 'bravo'}}, 222 | {alpha: 'alpha', bravo: {bravo: 'bravo'}} 223 | 224 | ], ["a", "b", {}]], 225 | obj: { 226 | nest: {alpha: 'alpha', bravo: {bravo: 'bravo'} } 227 | } 228 | }); 229 | }); 230 | 231 | it('should throw error if an invalid character in key', function() { 232 | expect(function() { 233 | $utils.toJSON({'foo.bar': 'foo.bar'}); 234 | }).toThrowError(Error); 235 | expect(function() { 236 | $utils.toJSON({'foo$bar': 'foo.bar'}); 237 | }).toThrowError(Error); 238 | expect(function() { 239 | $utils.toJSON({'foo#bar': 'foo.bar'}); 240 | }).toThrowError(Error); 241 | expect(function() { 242 | $utils.toJSON({'foo[bar': 'foo.bar'}); 243 | }).toThrowError(Error); 244 | expect(function() { 245 | $utils.toJSON({'foo]bar': 'foo.bar'}); 246 | }).toThrowError(Error); 247 | expect(function() { 248 | $utils.toJSON({'foo/bar': 'foo.bar'}); 249 | }).toThrowError(Error); 250 | }); 251 | 252 | it('should throw error if undefined value', function() { 253 | expect(function() { 254 | var undef; 255 | $utils.toJSON({foo: 'bar', baz: undef}); 256 | }).toThrowError(Error); 257 | }); 258 | }); 259 | 260 | describe('#getKey', function() { 261 | it('should return the key name given a DataSnapshot', function() { 262 | var snapshot = testutils.snap('data', 'foo'); 263 | expect($utils.getKey(snapshot)).toEqual('foo'); 264 | }); 265 | }); 266 | 267 | describe('#makeNodeResolver', function(){ 268 | var deferred, callback; 269 | beforeEach(function(){ 270 | deferred = jasmine.createSpyObj('promise',['resolve','reject']); 271 | callback = $utils.makeNodeResolver(deferred); 272 | }); 273 | 274 | it('should return a function', function(){ 275 | expect(callback).toBeA('function'); 276 | }); 277 | 278 | it('should reject the promise if the first argument is truthy', function(){ 279 | var error = new Error('blah'); 280 | callback(error); 281 | expect(deferred.reject).toHaveBeenCalledWith(error); 282 | }); 283 | 284 | it('should reject the promise if the first argument is not null', function(){ 285 | callback(false); 286 | expect(deferred.reject).toHaveBeenCalledWith(false); 287 | }); 288 | 289 | it('should resolve the promise if the first argument is null', function(){ 290 | var result = {data:'hello world'}; 291 | callback(null,result); 292 | expect(deferred.resolve).toHaveBeenCalledWith(result); 293 | }); 294 | 295 | it('should aggregate multiple arguments into an array', function(){ 296 | var result1 = {data:'hello world!'}; 297 | var result2 = {data:'howdy!'}; 298 | callback(null,result1,result2); 299 | expect(deferred.resolve).toHaveBeenCalledWith([result1,result2]); 300 | }); 301 | }); 302 | 303 | describe('#doSet', function() { 304 | var ref; 305 | beforeEach(function() { 306 | ref = new MockWilddog('Mock://').child('data/REC1'); 307 | }); 308 | 309 | it('returns a promise', function() { 310 | expect($utils.doSet(ref, null)).toBeAPromise(); 311 | }); 312 | 313 | it('resolves on success', function() { 314 | var whiteSpy = jasmine.createSpy('resolve'); 315 | var blackSpy = jasmine.createSpy('reject'); 316 | $utils.doSet(ref, {foo: 'bar'}).then(whiteSpy, blackSpy); 317 | ref.flush(); 318 | $timeout.flush(); 319 | expect(blackSpy).not.toHaveBeenCalled(); 320 | expect(whiteSpy).toHaveBeenCalled(); 321 | }); 322 | 323 | it('saves the data', function() { 324 | $utils.doSet(ref, true); 325 | ref.flush(); 326 | expect(ref.getData()).toBe(true); 327 | }); 328 | 329 | it('rejects promise when fails', function() { 330 | ref.failNext('set', new Error('setfail')); 331 | var whiteSpy = jasmine.createSpy('resolve'); 332 | var blackSpy = jasmine.createSpy('reject'); 333 | $utils.doSet(ref, {foo: 'bar'}).then(whiteSpy, blackSpy); 334 | ref.flush(); 335 | $timeout.flush(); 336 | expect(whiteSpy).not.toHaveBeenCalled(); 337 | expect(blackSpy).toHaveBeenCalledWith(new Error('setfail')); 338 | }); 339 | 340 | it('only affects query keys when using a query', function() { 341 | ref.set(MOCK_DATA); 342 | ref.flush(); 343 | var query = ref.limit(1); //todo-mock MockWilddog doesn't support 2.x queries yet 344 | spyOn(query.ref(), 'update'); 345 | var expKeys = query.slice().keys; 346 | $utils.doSet(query, {hello: 'world'}); 347 | query.flush(); 348 | var args = query.ref().update.calls.mostRecent().args[0]; 349 | expect(Object.keys(args)).toEqual(['hello'].concat(expKeys)); 350 | }); 351 | }); 352 | 353 | describe('#doRemove', function() { 354 | var ref; 355 | beforeEach(function() { 356 | ref = new MockWilddog('Mock://').child('data/REC1'); 357 | }); 358 | 359 | it('returns a promise', function() { 360 | expect($utils.doRemove(ref)).toBeAPromise(); 361 | }); 362 | 363 | it('resolves if successful', function() { 364 | var whiteSpy = jasmine.createSpy('resolve'); 365 | var blackSpy = jasmine.createSpy('reject'); 366 | $utils.doRemove(ref).then(whiteSpy, blackSpy); 367 | ref.flush(); 368 | $timeout.flush(); 369 | expect(blackSpy).not.toHaveBeenCalled(); 370 | expect(whiteSpy).toHaveBeenCalled(); 371 | }); 372 | 373 | it('removes the data', function() { 374 | $utils.doRemove(ref); 375 | ref.flush(); 376 | expect(ref.getData()).toBe(null); 377 | }); 378 | 379 | it('rejects promise if write fails', function() { 380 | var whiteSpy = jasmine.createSpy('resolve'); 381 | var blackSpy = jasmine.createSpy('reject'); 382 | var err = new Error('test_fail_remove'); 383 | ref.failNext('remove', err); 384 | $utils.doRemove(ref).then(whiteSpy, blackSpy); 385 | ref.flush(); 386 | $timeout.flush(); 387 | expect(whiteSpy).not.toHaveBeenCalled(); 388 | expect(blackSpy).toHaveBeenCalledWith(err); 389 | }); 390 | 391 | it('only removes keys in query when query is used', function() { 392 | ref.set(MOCK_DATA); 393 | ref.flush(); 394 | var query = ref.limit(2); //todo-mock MockWilddog does not support 2.x queries yet 395 | var keys = query.slice().keys; 396 | var origKeys = query.ref().getKeys(); 397 | expect(keys.length).toBeGreaterThan(0); 398 | expect(origKeys.length).toBeGreaterThan(keys.length); 399 | origKeys.forEach(function (key) { 400 | spyOn(query.ref().child(key), 'remove'); 401 | }); 402 | $utils.doRemove(query); 403 | query.flush(); 404 | keys.forEach(function(key) { 405 | expect(query.ref().child(key).remove).toHaveBeenCalled(); 406 | }); 407 | origKeys.forEach(function(key) { 408 | if( keys.indexOf(key) === -1 ) { 409 | expect(query.ref().child(key).remove).not.toHaveBeenCalled(); 410 | } 411 | }); 412 | }); 413 | 414 | it('waits to resolve promise until data is actually deleted',function(){ 415 | ref.set(MOCK_DATA); 416 | ref.flush(); 417 | var query = ref.limit(2); 418 | var resolved = false; 419 | $utils.doRemove(query).then(function(){ 420 | resolved = true; 421 | }); 422 | expect(resolved).toBe(false); 423 | ref.flush(); 424 | $timeout.flush(); 425 | expect(resolved).toBe(true); 426 | }); 427 | }); 428 | 429 | describe('#VERSION', function() { 430 | it('should return the version number', function() { 431 | expect($utils.VERSION).toEqual('0.0.0'); 432 | }); 433 | }); 434 | }); 435 | 436 | describe('#promise (ES6 Polyfill)', function(){ 437 | 438 | var status, result, reason, $utils, $timeout; 439 | 440 | function wrapPromise(promise){ 441 | promise.then(function(_result){ 442 | status = 'resolved'; 443 | result = _result; 444 | },function(_reason){ 445 | status = 'rejected'; 446 | reason = _reason; 447 | }); 448 | } 449 | 450 | beforeEach(function(){ 451 | status = 'pending'; 452 | result = null; 453 | reason = null; 454 | }); 455 | 456 | beforeEach(module('wilddog',function($provide){ 457 | $provide.decorator('$q',function($delegate){ 458 | //Forces polyfil even if we are testing against angular 1.3.x 459 | return { 460 | defer:$delegate.defer, 461 | all:$delegate.all 462 | } 463 | }); 464 | })); 465 | 466 | beforeEach(inject(function(_$wilddogUtils_, _$timeout_){ 467 | $utils = _$wilddogUtils_; 468 | $timeout = _$timeout_; 469 | })); 470 | 471 | it('throws an error if not called with a function',function(){ 472 | expect(function(){ 473 | $utils.promise(); 474 | }).toThrow(); 475 | expect(function(){ 476 | $utils.promise({}); 477 | }).toThrow(); 478 | }); 479 | 480 | it('calling resolve will resolve the promise with the provided result',function(){ 481 | wrapPromise(new $utils.promise(function(resolve,reject){ 482 | resolve('foo'); 483 | })); 484 | $timeout.flush(); 485 | expect(status).toBe('resolved'); 486 | expect(result).toBe('foo'); 487 | }); 488 | 489 | it('calling reject will reject the promise with the provided reason',function(){ 490 | wrapPromise(new $utils.promise(function(resolve,reject){ 491 | reject('bar'); 492 | })); 493 | $timeout.flush(); 494 | expect(status).toBe('rejected'); 495 | expect(reason).toBe('bar'); 496 | }); 497 | 498 | }); 499 | -------------------------------------------------------------------------------- /tests/unit/wilddog.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | describe('$wilddog', function () { 3 | 4 | beforeEach(function () { 5 | module('wilddog'); 6 | }); 7 | 8 | describe('', function () { 9 | var $wilddog; 10 | beforeEach(function() { 11 | inject(function (_$wilddog_) { 12 | $wilddog = _$wilddog_; 13 | }); 14 | }); 15 | it('throws an error', function() { 16 | expect(function() { 17 | $wilddog(new Wilddog('Mock://')); 18 | }).toThrow(); 19 | }); 20 | }); 21 | }); 22 | --------------------------------------------------------------------------------