├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bower.json ├── dist ├── angular-sails.js └── angular-sails.min.js ├── files.js ├── gulpfile.js ├── index.js ├── karma.conf.js ├── mock └── socket-io.js ├── package.json ├── src ├── ngSails.js └── service │ └── angular-sails.js └── test ├── angular-sails-provider.spec.js └── angular-sails-service.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | ###SublimeText### 2 | 3 | # SublimeText project files 4 | *.sublime-workspace 5 | 6 | 7 | ###OSX### 8 | 9 | .DS_Store 10 | .AppleDouble 11 | .LSOverride 12 | Icon 13 | 14 | 15 | # Thumbnails 16 | ._* 17 | 18 | # Files that might appear on external disk 19 | .Spotlight-V100 20 | .Trashes 21 | 22 | 23 | ###Windows### 24 | 25 | # Windows image file caches 26 | Thumbs.db 27 | ehthumbs.db 28 | 29 | # Folder config file 30 | Desktop.ini 31 | 32 | # Recycle Bin used on file shares 33 | $RECYCLE.BIN/ 34 | 35 | 36 | ###Node### 37 | 38 | lib-cov 39 | *.seed 40 | *.log 41 | *.csv 42 | *.dat 43 | *.out 44 | *.pid 45 | *.gz 46 | 47 | pids 48 | logs 49 | results 50 | 51 | npm-debug.log 52 | node_modules 53 | bower_components 54 | build 55 | 56 | .tern-project 57 | .idea 58 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | 5 | before_script: 6 | - npm install 7 | - npm install -g gulp 8 | - npm install -g bower 9 | - bower install -F 10 | 11 | script: 12 | - gulp build-js 13 | - gulp test-build 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Jan-Oliver Pantel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/janpantel/angular-sails.svg?branch=master)](https://travis-ci.org/janpantel/angular-sails) 2 | 3 | Angular Sails 4 | ============= 5 | 6 | This small module allows you to use Sails.JS's awesome socket.io api with AngularJS. 7 | 8 | Just add a dependency to your module and controllers and get it going! 9 | 10 | Install it: 11 | 12 | ```shell 13 | bower install angular-sails 14 | ``` 15 | You must also include [sails.io.js](https://github.com/balderdashy/sails.io.js) in order to use this. 16 | 17 | Usage 18 | ----- 19 | 20 | For a more complex example, have a look at the [tutorial by Maarten](https://github.com/maartendb/angular-sails-scrum-tutorial). 21 | 22 | A small example: 23 | 24 | ```javascript 25 | var app = angular.module("MyApp", ['ngSails']); 26 | 27 | //OPTIONAL! Set socket URL! 28 | app.config(['$sailsProvider', function ($sailsProvider) { 29 | $sailsProvider.url = 'http://foo.bar'; 30 | }]); 31 | 32 | app.controller("FooController", function ($scope, $sails) { 33 | $scope.bars = []; 34 | 35 | (function () { 36 | // Using .success() and .error() 37 | $sails.get("/bars") 38 | .success(function (data, status, headers, jwr) { 39 | $scope.bars = data; 40 | }) 41 | .error(function (data, status, headers, jwr) { 42 | alert('Houston, we got a problem!'); 43 | }); 44 | 45 | // Using .then() 46 | $sails.get("/bars") 47 | .then(function(resp){ 48 | $scope.bars = resp.data; 49 | }, function(resp){ 50 | alert('Houston, we got a problem!'); 51 | }); 52 | 53 | // Watching for updates 54 | var barsHandler = $sails.on("bars", function (message) { 55 | if (message.verb === "created") { 56 | $scope.bars.push(message.data); 57 | } 58 | }); 59 | 60 | // Stop watching for updates 61 | $scope.$on('$destroy', function() { 62 | $sails.off('bars', barsHandler); 63 | }); 64 | 65 | }()); 66 | }); 67 | ``` 68 | 69 | API Reference 70 | -------------- 71 | 72 | ### Sails.JS REST ### 73 | Angular Sails wraps the native sails.js REST functions. For further information check out [the sails docs](http://sailsjs.org/#!documentation/sockets) and [Mike's Screencast](http://www.youtube.com/watch?v=GK-tFvpIR7c) 74 | 75 | ### Native socket functions ### 76 | The sails service is nothing more like the native socket.io object! 77 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-sails", 3 | "version": "1.1.4", 4 | "authors": [ 5 | "Jan-Oliver Pantel ", 6 | "Evan Sharp " 7 | ], 8 | "description": "An angular provider for using the sails socket.io api", 9 | "main": "./dist/angular-sails.js", 10 | "keywords": [ 11 | "angular", 12 | "angularjs", 13 | "sails", 14 | "sailsjs", 15 | "socket.io", 16 | "provider", 17 | "api" 18 | ], 19 | "license": "MIT", 20 | "homepage": "https://github.com/kyjan/angular-sails", 21 | "dependencies": { 22 | "angular": ">=1.2.*", 23 | "sails.io.js": "*" 24 | }, 25 | "devDependencies":{ 26 | "angular-mocks": ">=1.2.*" 27 | }, 28 | "ignore": [ 29 | "**/.*", 30 | "gulpfile.js", 31 | "package.json", 32 | "node_modules", 33 | "build", 34 | "mock", 35 | "bower_components", 36 | "app/bower_components", 37 | "test", 38 | "tests" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /dist/angular-sails.js: -------------------------------------------------------------------------------- 1 | (function (angular, io) { 2 | 'use strict'/*global angular */ 3 | angular.module('ngSails', ['ng']); 4 | 5 | /*global angular, io */ 6 | (function(angular, io) { 7 | 'use strict'; 8 | if(io.sails){ 9 | io.sails.autoConnect = false; 10 | } 11 | 12 | // copied from angular 13 | function parseHeaders(headers) { 14 | var parsed = {}, 15 | key, val, i; 16 | if (!headers) return parsed; 17 | angular.forEach(headers.split('\n'), function(line) { 18 | i = line.indexOf(':'); 19 | key = lowercase(trim(line.substr(0, i))); 20 | val = trim(line.substr(i + 1)); 21 | if (key) { 22 | parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val; 23 | } 24 | }); 25 | 26 | return parsed; 27 | } 28 | 29 | function trim(value) { 30 | return angular.isString(value) ? value.trim() : value; 31 | } 32 | 33 | function isPromiseLike (obj){ 34 | return obj && angular.isFunction(obj.then); 35 | } 36 | 37 | // copied from angular 38 | function headersGetter(headers) { 39 | var headersObj = angular.isObject(headers) ? headers : undefined; 40 | return function(name) { 41 | if (!headersObj) headersObj = parseHeaders(headers); 42 | if (name) { 43 | var value = headersObj[lowercase(name)]; 44 | if (value === void 0) { 45 | value = null; 46 | } 47 | return value; 48 | } 49 | return headersObj; 50 | }; 51 | } 52 | 53 | angular.module('ngSails').provider('$sails', function() { 54 | var provider = this; 55 | 56 | this.httpVerbs = ['get', 'post', 'put', 'delete']; 57 | 58 | this.eventNames = ['on', 'off']; 59 | 60 | this.url = undefined; 61 | 62 | this.urlPrefix = ''; 63 | 64 | this.config = { 65 | transports: ['websocket', 'polling'], 66 | useCORSRouteToGetCookie: false 67 | }; 68 | 69 | this.debug = false; 70 | 71 | // like https://docs.angularjs.org/api/ng/service/$http#interceptors 72 | // but with sails.io arguments 73 | var interceptorFactories = this.interceptors = [ 74 | /*function($injectables) { 75 | return { 76 | request: function(config) {}, 77 | response: function(response) {}, 78 | requestError: function(rejection) {}, 79 | responseError: function(rejection) {} 80 | }; 81 | }*/ 82 | ]; 83 | 84 | /*@ngInject*/ 85 | this.$get = ["$q", "$injector", "$rootScope", "$log", "$timeout", function($q, $injector, $rootScope, $log, $timeout) { 86 | var socket = (io.sails && io.sails.connect || io.connect)(provider.url, provider.config); 87 | 88 | socket.connect = function(opts){ 89 | if(!socket.isConnected()){ 90 | var _opts = opts||{}; 91 | _opts = angular.extend({},provider.config,opts); 92 | 93 | // These are the options sails.io.js actually sets when making the connection. 94 | socket.useCORSRouteToGetCookie = _opts.useCORSRouteToGetCookie; 95 | socket.url = _opts.url || provider.url; 96 | socket.multiplex = _opts.multiplex; 97 | 98 | socket._connect(); 99 | } 100 | return socket; 101 | }; 102 | 103 | // TODO: separate out interceptors into its own file (and provider?). 104 | // build interceptor chain 105 | var reversedInterceptors = []; 106 | angular.forEach(interceptorFactories, function(interceptorFactory) { 107 | reversedInterceptors.unshift( 108 | angular.isString(interceptorFactory) ? 109 | $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory) 110 | ); 111 | }); 112 | 113 | // Send the request using the socket 114 | function serverRequest(config) { 115 | var defer = $q.defer(); 116 | if (provider.debug) $log.info('$sails ' + config.method + ' ' + config.url, config.data || ''); 117 | 118 | if (config.timeout > 0) { 119 | $timeout(timeoutRequest, config.timeout); 120 | } else if (isPromiseLike(config.timeout)) { 121 | config.timeout.then(timeoutRequest); 122 | } 123 | 124 | socket['legacy_' + config.method.toLowerCase()](config.url, config.data, serverResponse); 125 | 126 | function timeoutRequest(){ 127 | serverResponse(null); 128 | } 129 | 130 | function serverResponse(result, jwr) { 131 | 132 | if (!jwr) { 133 | jwr = { 134 | body: result, 135 | headers: result.headers || {}, 136 | statusCode: result.statusCode || result.status || 0, 137 | error: (function() { 138 | if (this.statusCode < 200 || this.statusCode >= 400) { 139 | return this.body || this.statusCode; 140 | } 141 | })() 142 | }; 143 | } 144 | 145 | jwr.data = jwr.body; // $http compat 146 | jwr.status = jwr.statusCode; // $http compat 147 | jwr.socket = socket; 148 | jwr.url = config.url; 149 | jwr.method = config.method; 150 | jwr.config = config.config; 151 | if (jwr.error) { 152 | if (provider.debug) $log.warn('$sails response ' + jwr.statusCode + ' ' + config.url, jwr); 153 | defer.reject(jwr); 154 | } else { 155 | if (provider.debug) $log.info('$sails response ' + config.url, jwr); 156 | defer.resolve(jwr); 157 | } 158 | } 159 | 160 | return defer.promise; 161 | } 162 | 163 | function promisify(methodName) { 164 | socket['legacy_' + methodName] = socket[methodName]; 165 | 166 | socket[methodName] = function(url, data, config) { 167 | 168 | var chain = [serverRequest, undefined]; 169 | 170 | //TODO: more compatible with $http methods and config 171 | 172 | var promise = $q.when({ 173 | url: provider.urlPrefix + url, 174 | data: data, 175 | socket: socket, 176 | config: config || {}, 177 | method: methodName.toUpperCase() 178 | }); 179 | 180 | // apply interceptors 181 | angular.forEach(reversedInterceptors, function(interceptor) { 182 | if (interceptor.request || interceptor.requestError) { 183 | chain.unshift(interceptor.request, interceptor.requestError); 184 | } 185 | if (interceptor.response || interceptor.responseError) { 186 | chain.push(interceptor.response, interceptor.responseError); 187 | } 188 | }); 189 | 190 | while (chain.length) { 191 | var thenFn = chain.shift(); 192 | var rejectFn = chain.shift(); 193 | 194 | promise = promise.then(thenFn, rejectFn); 195 | } 196 | 197 | // be $http compatible 198 | promise.success = function(fn) { 199 | promise.then(function(jwr) { 200 | fn(jwr.body, jwr.statusCode, headersGetter(jwr.headers), jwr); 201 | }); 202 | return promise; 203 | }; 204 | promise.error = function(fn) { 205 | promise.then(null, function(jwr) { 206 | fn(jwr.body, jwr.statusCode, headersGetter(jwr.headers), jwr); 207 | }); 208 | return promise; 209 | }; 210 | 211 | return promise; 212 | }; 213 | } 214 | 215 | function wrapEvent(eventName) { 216 | if(socket[eventName] || socket._raw && socket._raw[eventName]) { 217 | socket['legacy_' + eventName] = socket[eventName] || socket._raw[eventName]; 218 | socket[eventName] = function(event, cb) { 219 | var wrapEventFn = null; 220 | if (eventName == 'off') { 221 | return socket['legacy_' + eventName](event, cb); 222 | }else if (cb !== null && angular.isFunction(cb)) { 223 | socket['legacy_' + eventName](event, wrapEventFn = function(result) { 224 | $rootScope.$evalAsync(cb.bind(socket, result)); 225 | }); 226 | } 227 | return wrapEventFn; 228 | }; 229 | } 230 | } 231 | 232 | // sails.io.js doesn't have `once`, need to access it through `._raw` 233 | socket.once = function(event, cb){ 234 | if (cb !== null && angular.isFunction(cb)) { 235 | if(socket._raw){ 236 | socket._raw.once(event, function(result) { 237 | $rootScope.$evalAsync(cb.bind(socket, result)); 238 | }); 239 | } 240 | } 241 | }; 242 | 243 | angular.forEach(provider.httpVerbs, promisify); 244 | angular.forEach(provider.eventNames, wrapEvent); 245 | 246 | 247 | /** 248 | * Update a model on sails pushes 249 | * @param {String} name Sails model name 250 | * @param {Array} models Array with model objects 251 | */ 252 | socket.$modelUpdater = function(name, models) { 253 | 254 | var update = function(message) { 255 | 256 | $rootScope.$evalAsync(function(){ 257 | var i; 258 | 259 | switch (message.verb) { 260 | 261 | case "created": 262 | // create new model item 263 | models.push(message.data); 264 | break; 265 | 266 | case "updated": 267 | var obj; 268 | for (i = 0; i < models.length; i++) { 269 | if (models[i].id === message.id) { 270 | obj = models[i]; 271 | break; 272 | } 273 | } 274 | 275 | // cant update if the angular-model does not have the item and the 276 | // sails message does not give us the previous record 277 | if (!obj && !message.previous) return; 278 | 279 | if (!obj) { 280 | // sails has given us the previous record, create it in our model 281 | obj = message.previous; 282 | models.push(obj); 283 | } 284 | 285 | // update the model item 286 | angular.extend(obj, message.data); 287 | break; 288 | 289 | case "destroyed": 290 | for (i = 0; i < models.length; i++) { 291 | if (models[i].id === message.id) { 292 | models.splice(i, 1); 293 | break; 294 | } 295 | } 296 | break; 297 | } 298 | }); 299 | }; 300 | 301 | socket.legacy_on(name, update); 302 | 303 | return function(){ 304 | socket.legacy_off(name, update); 305 | }; 306 | }; 307 | 308 | return socket; 309 | }]; 310 | this.$get.$inject = ["$q", "$injector", "$rootScope", "$log", "$timeout"]; 311 | }); 312 | }(angular, io)); 313 | }(angular, io)); -------------------------------------------------------------------------------- /dist/angular-sails.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"use strict";e.module("ngSails",["ng"]),function(e,t){function n(t){var n,r,s,i={};return t?(e.forEach(t.split("\n"),function(e){s=e.indexOf(":"),n=lowercase(o(e.substr(0,s))),r=o(e.substr(s+1)),n&&(i[n]=i[n]?i[n]+", "+r:r)}),i):i}function o(t){return e.isString(t)?t.trim():t}function r(t){return t&&e.isFunction(t.then)}function s(t){var o=e.isObject(t)?t:void 0;return function(e){if(o||(o=n(t)),e){var r=o[lowercase(e)];return void 0===r&&(r=null),r}return o}}t.sails&&(t.sails.autoConnect=!1),e.module("ngSails").provider("$sails",function(){var n=this;this.httpVerbs=["get","post","put","delete"],this.eventNames=["on","off"],this.url=void 0,this.urlPrefix="",this.config={transports:["websocket","polling"],useCORSRouteToGetCookie:!1},this.debug=!1;var o=this.interceptors=[];this.$get=["$q","$injector","$rootScope","$log","$timeout",function(i,u,a,c,f){function l(e){function t(){o(null)}function o(t,o){o||(o={body:t,headers:t.headers||{},statusCode:t.statusCode||t.status||0,error:function(){return this.statusCode<200||this.statusCode>=400?this.body||this.statusCode:void 0}()}),o.data=o.body,o.status=o.statusCode,o.socket=g,o.url=e.url,o.method=e.method,o.config=e.config,o.error?(n.debug&&c.warn("$sails response "+o.statusCode+" "+e.url,o),s.reject(o)):(n.debug&&c.info("$sails response "+e.url,o),s.resolve(o))}var s=i.defer();return n.debug&&c.info("$sails "+e.method+" "+e.url,e.data||""),e.timeout>0?f(t,e.timeout):r(e.timeout)&&e.timeout.then(t),g["legacy_"+e.method.toLowerCase()](e.url,e.data,o),s.promise}function d(t){g["legacy_"+t]=g[t],g[t]=function(o,r,u){var a=[l,void 0],c=i.when({url:n.urlPrefix+o,data:r,socket:g,config:u||{},method:t.toUpperCase()});for(e.forEach(v,function(e){(e.request||e.requestError)&&a.unshift(e.request,e.requestError),(e.response||e.responseError)&&a.push(e.response,e.responseError)});a.length;){var f=a.shift(),d=a.shift();c=c.then(f,d)}return c.success=function(e){return c.then(function(t){e(t.body,t.statusCode,s(t.headers),t)}),c},c.error=function(e){return c.then(null,function(t){e(t.body,t.statusCode,s(t.headers),t)}),c},c}}function h(t){(g[t]||g._raw&&g._raw[t])&&(g["legacy_"+t]=g[t]||g._raw[t],g[t]=function(n,o){var r=null;return"off"==t?g["legacy_"+t](n,o):(null!==o&&e.isFunction(o)&&g["legacy_"+t](n,r=function(e){a.$evalAsync(o.bind(g,e))}),r)})}var g=(t.sails&&t.sails.connect||t.connect)(n.url,n.config);g.connect=function(t){if(!g.isConnected()){var o=t||{};o=e.extend({},n.config,t),g.useCORSRouteToGetCookie=o.useCORSRouteToGetCookie,g.url=o.url||n.url,g.multiplex=o.multiplex,g._connect()}return g};var v=[];return e.forEach(o,function(t){v.unshift(e.isString(t)?u.get(t):u.invoke(t))}),g.once=function(t,n){null!==n&&e.isFunction(n)&&g._raw&&g._raw.once(t,function(e){a.$evalAsync(n.bind(g,e))})},e.forEach(n.httpVerbs,d),e.forEach(n.eventNames,h),g.$modelUpdater=function(t,n){var o=function(t){a.$evalAsync(function(){var o;switch(t.verb){case"created":n.push(t.data);break;case"updated":var r;for(o=0;o -1) { 56 | this._listeners[ev].splice(index, 1); 57 | } 58 | } else { 59 | delete this._listeners[ev]; 60 | } 61 | }, 62 | off: function(ev, fn) { 63 | if (fn) { 64 | var index = this._listeners[ev].indexOf(fn); 65 | if (index > -1) { 66 | this._listeners[ev].splice(index, 1); 67 | } 68 | } else { 69 | delete this._listeners[ev]; 70 | } 71 | }, 72 | removeAllListeners: function(ev) { 73 | if (ev) { 74 | delete this._listeners[ev]; 75 | } else { 76 | this._listeners = {}; 77 | } 78 | }, 79 | disconnect: function() { 80 | this.connected = false; 81 | this.emit('disconnect'); 82 | }, 83 | connect: function() { 84 | this.connected = true; 85 | this.emit('connect'); 86 | return this; 87 | } 88 | }; 89 | 90 | return socket; 91 | } 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-sails", 3 | "version": "1.1.4", 4 | "description": "An angular provider for using the sails socket.io api", 5 | "scripts": { 6 | "build": "gulp build-js", 7 | "test": "gulp test" 8 | }, 9 | "main": "index.js", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/janpantel/angular-sails.git" 13 | }, 14 | "author": "Jan-Oliver Pantel ", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/kyjan/angular-sails/issues" 18 | }, 19 | "homepage": "https://github.com/kyjan/angular-sails", 20 | "dependencies": { 21 | "angular": "^1.3.0", 22 | "sails.io.js": "^0.11.0" 23 | }, 24 | "devDependencies": { 25 | "chai": "^1.10.0", 26 | "gulp": "^3.5.6", 27 | "gulp-concat": "^2.2.0", 28 | "gulp-footer": "^1.0.4", 29 | "gulp-header": "^1.0.2", 30 | "gulp-karma": "0.0.4", 31 | "gulp-ng-annotate": "^0.3.3", 32 | "gulp-uglify": "^0.2.1", 33 | "karma": "^0.12.21", 34 | "karma-chai-sinon": "^0.1.4", 35 | "karma-chrome-launcher": "~0.1.0", 36 | "karma-firefox-launcher": "~0.1.0", 37 | "karma-mocha": "~0.1", 38 | "karma-phantomjs-launcher": "~0.1.0", 39 | "karma-script-launcher": "~0.1.0", 40 | "sinon": "^1.12.2", 41 | "sinon-chai": "^2.6.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/ngSails.js: -------------------------------------------------------------------------------- 1 | /*global angular */ 2 | angular.module('ngSails', ['ng']); 3 | -------------------------------------------------------------------------------- /src/service/angular-sails.js: -------------------------------------------------------------------------------- 1 | /*global angular, io */ 2 | (function(angular, io) { 3 | 'use strict'; 4 | if(io.sails){ 5 | io.sails.autoConnect = false; 6 | } 7 | 8 | // copied from angular 9 | function parseHeaders(headers) { 10 | var parsed = {}, 11 | key, val, i; 12 | if (!headers) return parsed; 13 | angular.forEach(headers.split('\n'), function(line) { 14 | i = line.indexOf(':'); 15 | key = lowercase(trim(line.substr(0, i))); 16 | val = trim(line.substr(i + 1)); 17 | if (key) { 18 | parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val; 19 | } 20 | }); 21 | 22 | return parsed; 23 | } 24 | 25 | function trim(value) { 26 | return angular.isString(value) ? value.trim() : value; 27 | } 28 | 29 | function isPromiseLike (obj){ 30 | return obj && angular.isFunction(obj.then); 31 | } 32 | 33 | // copied from angular 34 | function headersGetter(headers) { 35 | var headersObj = angular.isObject(headers) ? headers : undefined; 36 | return function(name) { 37 | if (!headersObj) headersObj = parseHeaders(headers); 38 | if (name) { 39 | var value = headersObj[lowercase(name)]; 40 | if (value === void 0) { 41 | value = null; 42 | } 43 | return value; 44 | } 45 | return headersObj; 46 | }; 47 | } 48 | 49 | angular.module('ngSails').provider('$sails', function() { 50 | var provider = this; 51 | 52 | this.httpVerbs = ['get', 'post', 'put', 'delete']; 53 | 54 | this.eventNames = ['on', 'off']; 55 | 56 | this.url = undefined; 57 | 58 | this.urlPrefix = ''; 59 | 60 | this.config = { 61 | transports: ['polling', 'websocket'], 62 | useCORSRouteToGetCookie: false 63 | }; 64 | 65 | this.debug = false; 66 | 67 | // like https://docs.angularjs.org/api/ng/service/$http#interceptors 68 | // but with sails.io arguments 69 | var interceptorFactories = this.interceptors = [ 70 | /*function($injectables) { 71 | return { 72 | request: function(config) {}, 73 | response: function(response) {}, 74 | requestError: function(rejection) {}, 75 | responseError: function(rejection) {} 76 | }; 77 | }*/ 78 | ]; 79 | 80 | /*@ngInject*/ 81 | this.$get = function($q, $injector, $rootScope, $log, $timeout) { 82 | var socket = (io.sails && io.sails.connect || io.connect)(provider.url, provider.config); 83 | 84 | socket.connect = function(opts){ 85 | if(!socket.isConnected()){ 86 | var _opts = opts||{}; 87 | _opts = angular.extend({},provider.config,opts); 88 | 89 | // These are the options sails.io.js actually sets when making the connection. 90 | socket.useCORSRouteToGetCookie = _opts.useCORSRouteToGetCookie; 91 | socket.url = _opts.url || provider.url; 92 | socket.multiplex = _opts.multiplex; 93 | 94 | socket._connect(); 95 | } 96 | return socket; 97 | }; 98 | 99 | // TODO: separate out interceptors into its own file (and provider?). 100 | // build interceptor chain 101 | var reversedInterceptors = []; 102 | angular.forEach(interceptorFactories, function(interceptorFactory) { 103 | reversedInterceptors.unshift( 104 | angular.isString(interceptorFactory) ? 105 | $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory) 106 | ); 107 | }); 108 | 109 | // Send the request using the socket 110 | function serverRequest(config) { 111 | var defer = $q.defer(); 112 | if (provider.debug) $log.info('$sails ' + config.method + ' ' + config.url, config.data || ''); 113 | 114 | if (config.timeout > 0) { 115 | $timeout(timeoutRequest, config.timeout); 116 | } else if (isPromiseLike(config.timeout)) { 117 | config.timeout.then(timeoutRequest); 118 | } 119 | 120 | socket['legacy_' + config.method.toLowerCase()](config.url, config.data, serverResponse); 121 | 122 | function timeoutRequest(){ 123 | serverResponse(null); 124 | } 125 | 126 | function serverResponse(result, jwr) { 127 | 128 | if (!jwr) { 129 | jwr = { 130 | body: result, 131 | headers: result.headers || {}, 132 | statusCode: result.statusCode || result.status || 0, 133 | error: (function() { 134 | if (this.statusCode < 200 || this.statusCode >= 400) { 135 | return this.body || this.statusCode; 136 | } 137 | })() 138 | }; 139 | } 140 | 141 | jwr.data = jwr.body; // $http compat 142 | jwr.status = jwr.statusCode; // $http compat 143 | jwr.socket = socket; 144 | jwr.url = config.url; 145 | jwr.method = config.method; 146 | jwr.config = config.config; 147 | if (jwr.error) { 148 | if (provider.debug) $log.warn('$sails response ' + jwr.statusCode + ' ' + config.url, jwr); 149 | defer.reject(jwr); 150 | } else { 151 | if (provider.debug) $log.info('$sails response ' + config.url, jwr); 152 | defer.resolve(jwr); 153 | } 154 | } 155 | 156 | return defer.promise; 157 | } 158 | 159 | function promisify(methodName) { 160 | socket['legacy_' + methodName] = socket[methodName]; 161 | 162 | socket[methodName] = function(url, data, config) { 163 | 164 | var chain = [serverRequest, undefined]; 165 | 166 | //TODO: more compatible with $http methods and config 167 | 168 | var promise = $q.when({ 169 | url: provider.urlPrefix + url, 170 | data: data, 171 | socket: socket, 172 | config: config || {}, 173 | method: methodName.toUpperCase() 174 | }); 175 | 176 | // apply interceptors 177 | angular.forEach(reversedInterceptors, function(interceptor) { 178 | if (interceptor.request || interceptor.requestError) { 179 | chain.unshift(interceptor.request, interceptor.requestError); 180 | } 181 | if (interceptor.response || interceptor.responseError) { 182 | chain.push(interceptor.response, interceptor.responseError); 183 | } 184 | }); 185 | 186 | while (chain.length) { 187 | var thenFn = chain.shift(); 188 | var rejectFn = chain.shift(); 189 | 190 | promise = promise.then(thenFn, rejectFn); 191 | } 192 | 193 | // be $http compatible 194 | promise.success = function(fn) { 195 | promise.then(function(jwr) { 196 | fn(jwr.body, jwr.statusCode, headersGetter(jwr.headers), jwr); 197 | }); 198 | return promise; 199 | }; 200 | promise.error = function(fn) { 201 | promise.then(null, function(jwr) { 202 | fn(jwr.body, jwr.statusCode, headersGetter(jwr.headers), jwr); 203 | }); 204 | return promise; 205 | }; 206 | 207 | return promise; 208 | }; 209 | } 210 | 211 | function wrapEvent(eventName) { 212 | if(socket[eventName] || socket._raw && socket._raw[eventName]) { 213 | socket['legacy_' + eventName] = socket[eventName] || socket._raw[eventName]; 214 | socket[eventName] = function(event, cb) { 215 | var wrapEventFn = null; 216 | if (eventName == 'off') { 217 | return socket['legacy_' + eventName](event, cb); 218 | }else if (cb !== null && angular.isFunction(cb)) { 219 | socket['legacy_' + eventName](event, wrapEventFn = function(result) { 220 | $rootScope.$evalAsync(cb.bind(socket, result)); 221 | }); 222 | } 223 | return wrapEventFn; 224 | }; 225 | } 226 | } 227 | 228 | // sails.io.js doesn't have `once`, need to access it through `._raw` 229 | socket.once = function(event, cb){ 230 | if (cb !== null && angular.isFunction(cb)) { 231 | if(socket._raw){ 232 | socket._raw.once(event, function(result) { 233 | $rootScope.$evalAsync(cb.bind(socket, result)); 234 | }); 235 | } 236 | } 237 | }; 238 | 239 | angular.forEach(provider.httpVerbs, promisify); 240 | angular.forEach(provider.eventNames, wrapEvent); 241 | 242 | 243 | /** 244 | * Update a model on sails pushes 245 | * @param {String} name Sails model name 246 | * @param {Array} models Array with model objects 247 | */ 248 | socket.$modelUpdater = function(name, models) { 249 | 250 | var update = function(message) { 251 | 252 | $rootScope.$evalAsync(function(){ 253 | var i; 254 | 255 | switch (message.verb) { 256 | 257 | case "created": 258 | // create new model item 259 | models.push(message.data); 260 | break; 261 | 262 | case "updated": 263 | var obj; 264 | for (i = 0; i < models.length; i++) { 265 | if (models[i].id === message.id) { 266 | obj = models[i]; 267 | break; 268 | } 269 | } 270 | 271 | // cant update if the angular-model does not have the item and the 272 | // sails message does not give us the previous record 273 | if (!obj && !message.previous) return; 274 | 275 | if (!obj) { 276 | // sails has given us the previous record, create it in our model 277 | obj = message.previous; 278 | models.push(obj); 279 | } 280 | 281 | // update the model item 282 | angular.extend(obj, message.data); 283 | break; 284 | 285 | case "destroyed": 286 | for (i = 0; i < models.length; i++) { 287 | if (models[i].id === message.id) { 288 | models.splice(i, 1); 289 | break; 290 | } 291 | } 292 | break; 293 | } 294 | }); 295 | }; 296 | 297 | socket.legacy_on(name, update); 298 | 299 | return function(){ 300 | socket.legacy_off(name, update); 301 | }; 302 | }; 303 | 304 | return socket; 305 | }; 306 | }); 307 | }(angular, io)); 308 | -------------------------------------------------------------------------------- /test/angular-sails-provider.spec.js: -------------------------------------------------------------------------------- 1 | describe('Agnular Sails provider', function() { 2 | 3 | var spy, 4 | requestSpy, 5 | responseSpy, 6 | requestErrorSpy, 7 | responseErrorSpy; 8 | methods = ['get', 'post', 'put', 'delete'], 9 | response = { 10 | success: { 11 | body: 'Success!', 12 | statusCode: 200 13 | }, 14 | error: { 15 | body: 'Error!', 16 | statusCode: 500 17 | } 18 | }; 19 | 20 | beforeEach(function(){ 21 | spy = sinon.spy(); 22 | requestSpy = sinon.spy(); 23 | responseSpy = sinon.spy(); 24 | requestErrorSpy = sinon.spy(); 25 | responseErrorSpy = sinon.spy(); 26 | socketRequestSpy = sinon.spy(); 27 | }); 28 | 29 | describe('interceptors', function() { 30 | 31 | describe('', function(){ 32 | 33 | var $sailsProvider, 34 | $scope, 35 | $sails, 36 | mockIoSocket; 37 | 38 | beforeEach(module('ngSails',function(_$sailsProvider_){ 39 | $sailsProvider = _$sailsProvider_; 40 | 41 | $sailsProvider.interceptors.push(function($q){ 42 | return { 43 | request: function(config){ 44 | requestSpy(); 45 | return config; 46 | }, 47 | response: function(response){ 48 | responseSpy(); 49 | return response; 50 | }, 51 | requestError: function(rejection){ 52 | requestErrorSpy(); 53 | return $q.reject(rejection); 54 | }, 55 | responseError: function(rejection){ 56 | responseErrorSpy(); 57 | return $q.reject(rejection); 58 | } 59 | }; 60 | }); 61 | })); 62 | 63 | beforeEach(inject(function(_$rootScope_, _$sails_) { 64 | $scope = _$rootScope_; 65 | $sails = _$sails_; 66 | mockIoSocket = $sails._raw; 67 | 68 | mockIoSocket.on('get', function(ctx, cb){ 69 | socketRequestSpy(); 70 | cb(response[ctx.url]); 71 | }); 72 | })); 73 | 74 | it('should call request callback before socket request', function () { 75 | $sails.get('success'); 76 | $scope.$digest(); 77 | 78 | expect(requestSpy).to.have.been.calledOnce; 79 | expect(requestSpy).to.have.been.calledBefore(socketRequestSpy); 80 | 81 | }); 82 | 83 | it('should call response callback after socket request, before callback', function () { 84 | $sails.get('success')['finally'](spy); 85 | $scope.$digest(); 86 | 87 | expect(responseSpy).to.have.been.calledOnce; 88 | expect(responseSpy).to.have.been.calledAfter(socketRequestSpy); 89 | expect(responseSpy).to.have.been.calledBefore(spy); 90 | 91 | }); 92 | 93 | 94 | it('should call errorResponse callback after socket request, before callback', function () { 95 | $sails.get('error')['finally'](spy); 96 | $scope.$digest(); 97 | 98 | expect(responseErrorSpy).to.have.been.calledOnce; 99 | expect(responseErrorSpy).to.have.been.calledAfter(socketRequestSpy); 100 | expect(responseErrorSpy).to.have.been.calledBefore(spy); 101 | 102 | }); 103 | 104 | 105 | }); 106 | 107 | describe('', function(){ 108 | 109 | it('should not make socket request when request is rejected', function () { 110 | 111 | module('ngSails',function($sailsProvider){ 112 | $sailsProvider.interceptors.push(function($q){ 113 | return { 114 | request: function(config){ 115 | return $q.reject('rejected'); 116 | } 117 | }; 118 | }); 119 | }); 120 | 121 | inject(function($rootScope, $sails) { 122 | $sails._raw.on('get', function(ctx, cb){ 123 | socketRequestSpy(); 124 | cb(response[ctx.url]); 125 | }); 126 | 127 | $sails.get('success'); 128 | $rootScope.$digest(); 129 | 130 | expect(socketRequestSpy).to.have.been.not.called; 131 | }); 132 | 133 | }); 134 | 135 | it('should call error callback when response is rejected', function () { 136 | 137 | module('ngSails',function($sailsProvider){ 138 | $sailsProvider.interceptors.push(function($q){ 139 | return { 140 | response: function(config){ 141 | return $q.reject('rejected'); 142 | } 143 | }; 144 | }); 145 | }); 146 | 147 | inject(function($rootScope, $sails) { 148 | var errorSpy = sinon.spy(); 149 | $sails._raw.on('get', function(ctx, cb){ 150 | socketRequestSpy(); 151 | cb(response[ctx.url]); 152 | }); 153 | 154 | $sails.get('success').then(errorSpy, spy); 155 | $rootScope.$digest(); 156 | 157 | expect(errorSpy).to.have.been.not.called; 158 | expect(spy).to.have.been.calledOnce; 159 | }); 160 | 161 | }); 162 | 163 | it('should pass request config', function () { 164 | 165 | module('ngSails',function($sailsProvider){ 166 | $sailsProvider.interceptors.push(function($q){ 167 | return { 168 | request: function(config){ 169 | expect(config.url).to.equal('success'); 170 | expect(config.method).to.equal('POST'); 171 | expect(config.data).to.deep.equal({value: true}) 172 | spy(); 173 | return config; 174 | } 175 | }; 176 | }); 177 | }); 178 | 179 | inject(function($rootScope, $sails) { 180 | var errorSpy = sinon.spy(); 181 | $sails._raw.on('post', function(ctx, cb){ 182 | socketRequestSpy(); 183 | cb(response[ctx.url]); 184 | }); 185 | 186 | $sails.post('success',{value: true}); 187 | $rootScope.$digest(); 188 | 189 | expect(spy).to.have.been.calledOnce; 190 | }); 191 | 192 | }); 193 | 194 | it('should allow manipulation of request', function () { 195 | 196 | module('ngSails',function($sailsProvider){ 197 | $sailsProvider.interceptors.push(function($q){ 198 | return { 199 | request: function(config){ 200 | config.url = 'success'; 201 | config.method = 'POST'; 202 | config.data = {value: true}; 203 | return config; 204 | } 205 | }; 206 | }); 207 | }); 208 | 209 | inject(function($rootScope, $sails) { 210 | var errorSpy = sinon.spy(); 211 | $sails._raw.on('post', function(ctx, cb){ 212 | expect(ctx.method).to.equal('post'); 213 | expect(ctx.url).to.equal('success'); 214 | expect(ctx.data).to.deep.equal({value: true}); 215 | socketRequestSpy(); 216 | cb(response[ctx.url]); 217 | }); 218 | 219 | $sails.get('error', {value: false}); 220 | $rootScope.$digest(); 221 | 222 | expect(socketRequestSpy).to.have.been.calledOnce; 223 | }); 224 | 225 | }); 226 | 227 | it('should verify order of execution', function() { 228 | var outerReq = sinon.spy(), 229 | outerRes = sinon.spy(), 230 | innerReq = sinon.spy(), 231 | innerRes = sinon.spy(); 232 | module('ngSails',function($sailsProvider){ 233 | $sailsProvider.interceptors.push(function() { 234 | return { 235 | request: function(config) { 236 | config.url += 'Outer'; 237 | outerReq(); 238 | return config; 239 | }, 240 | response: function(response) { 241 | response.data = '{' + response.data + '} outer'; 242 | outerRes(); 243 | return response; 244 | } 245 | }; 246 | }); 247 | $sailsProvider.interceptors.push(function() { 248 | return { 249 | request: function(config) { 250 | config.url += 'Inner'; 251 | innerReq(); 252 | return config; 253 | }, 254 | response: function(response) { 255 | response.data = '{' + response.data + '} inner'; 256 | innerRes(); 257 | return response; 258 | } 259 | }; 260 | }); 261 | }); 262 | 263 | inject(function($rootScope, $sails) { 264 | $sails._raw.on('get', function(ctx, cb){ 265 | cb(response.success); 266 | }); 267 | 268 | $sails.get('success').then(function(res){ 269 | expect(res.method).to.equal('GET'); 270 | expect(res.url).to.equal('successOuterInner'); 271 | expect(res.data).to.equal('{{Success!} inner} outer'); 272 | spy() 273 | }); 274 | $rootScope.$digest(); 275 | 276 | expect(spy).to.have.been.calledOnce; 277 | expect(outerReq).to.have.been.calledBefore(innerReq); 278 | expect(innerRes).to.have.been.calledBefore(outerRes); 279 | }); 280 | }); 281 | 282 | }); 283 | 284 | }); 285 | 286 | }); 287 | -------------------------------------------------------------------------------- /test/angular-sails-service.spec.js: -------------------------------------------------------------------------------- 1 | describe('Agnular Sails service', function() { 2 | 3 | var $scope, 4 | $sails, 5 | mockIoSocket, 6 | spy, 7 | methods = ['get', 'post', 'put', 'delete'], 8 | response = { 9 | success: { 10 | body: 'Success!', 11 | statusCode: 200 12 | }, 13 | error: { 14 | body: 'Error!', 15 | statusCode: 500 16 | } 17 | }; 18 | 19 | beforeEach(module('ngSails')); 20 | 21 | beforeEach(inject(function(_$rootScope_, _$sails_) { 22 | $scope = _$rootScope_; 23 | $sails = _$sails_; 24 | mockIoSocket = $sails._raw; 25 | spy = sinon.spy(); 26 | })); 27 | 28 | describe('connection', function() { 29 | 30 | it('should be able to connect and disconnect', function () { 31 | 32 | expect($sails.isConnected()).to.be.true(); 33 | 34 | $sails.disconnect(); 35 | 36 | expect($sails.isConnected()).to.be.false(); 37 | 38 | $sails.connect(); 39 | 40 | expect($sails.isConnected()).to.be.true(); 41 | 42 | $sails.disconnect(); 43 | 44 | expect($sails.isConnected()).to.be.false(); 45 | 46 | $sails.connect(); 47 | 48 | expect($sails.isConnected()).to.be.true(); 49 | }); 50 | 51 | 52 | it('should queue up requests', function () { 53 | var getSpy = sinon.spy(), 54 | postSpy = sinon.spy(), 55 | getCbSpy = sinon.spy(), 56 | postCbSpy = sinon.spy(); 57 | 58 | $sails.disconnect(); 59 | 60 | mockIoSocket.on('get', function(ctx, cb){ 61 | getSpy(); 62 | cb(response[ctx.url]); 63 | }); 64 | mockIoSocket.on('post', function(ctx, cb){ 65 | postSpy(); 66 | cb(response[ctx.url]); 67 | }); 68 | 69 | $sails.get('success').then(getCbSpy); 70 | $sails.post('success').then(postCbSpy); 71 | 72 | $scope.$digest(); 73 | 74 | expect(getSpy).to.have.been.not.called; 75 | expect(postSpy).to.have.been.not.called; 76 | expect(getCbSpy).to.have.been.not.called; 77 | expect(postCbSpy).to.have.been.not.called; 78 | 79 | $sails.connect(); 80 | 81 | /* TODO: Someone determine how to test this 82 | Because `$sails.connect()` automatically runs the queued calls on the new socket, 83 | We cannot grab the other end of the new socket to watch for the calls and respond 84 | to the calls.... This makes this really hard to test. In fact, from what I can 85 | tell impossible to test. 86 | */ 87 | // mockIoSocket = $sails._raw; 88 | // 89 | // mockIoSocket.on('get', function(ctx, cb){ 90 | // getSpy(); 91 | // cb(response[ctx.url]); 92 | // }); 93 | // mockIoSocket.on('post', function(ctx, cb){ 94 | // postSpy(); 95 | // cb(response[ctx.url]); 96 | // }); 97 | // 98 | // $scope.$digest(); 99 | // 100 | // expect(getSpy).to.have.been.calledOnce; 101 | // expect(postSpy).to.have.been.calledOnce; 102 | // expect(getCbSpy).to.have.been.calledOnce; 103 | // expect(postCbSpy).to.have.been.calledOnce; 104 | }); 105 | 106 | }); 107 | 108 | 109 | describe('on', function() { 110 | 111 | it('should apply asynchronously', function () { 112 | $sails.on('event', spy); 113 | mockIoSocket.emit('event'); 114 | 115 | expect(spy).to.have.been.not.called; 116 | $scope.$digest(); 117 | 118 | expect(spy).to.have.been.calledOnce; 119 | }); 120 | 121 | it('should allow multiple listeners for the same event', function () { 122 | var eventSpy = sinon.spy() 123 | $sails.on('event', spy); 124 | $sails.on('event', eventSpy); 125 | mockIoSocket.emit('event'); 126 | $scope.$digest(); 127 | 128 | expect(spy).to.have.been.calledOnce; 129 | expect(eventSpy).to.have.been.calledOnce; 130 | }); 131 | 132 | it('should call the correct lisener', function () { 133 | var eventSpy = sinon.spy() 134 | $sails.on('event', spy); 135 | $sails.on('anotherEvent', eventSpy); 136 | mockIoSocket.emit('event'); 137 | $scope.$digest(); 138 | 139 | expect(spy).to.have.been.calledOnce; 140 | expect(eventSpy).to.have.not.been.called; 141 | }); 142 | 143 | }); 144 | 145 | describe('off', function() { 146 | 147 | describe('by event name only', function(){ 148 | 149 | it('should remove all event listener by the given name', function () { 150 | var eventSpy = sinon.spy() 151 | $sails.on('event', spy); 152 | $sails.on('event', eventSpy); 153 | $sails.off('event'); 154 | mockIoSocket.emit('event'); 155 | $scope.$digest(); 156 | 157 | expect(spy).to.have.not.been.called; 158 | expect(eventSpy).to.have.not.been.called; 159 | }); 160 | 161 | it('should only the listeners that match the given event name', function () { 162 | var eventSpy = sinon.spy() 163 | var anotherEventSpy = sinon.spy() 164 | $sails.on('event', spy); 165 | $sails.on('event', eventSpy); 166 | $sails.on('anotherEvent', anotherEventSpy); 167 | 168 | $sails.off('event'); 169 | mockIoSocket.emit('event'); 170 | mockIoSocket.emit('anotherEvent'); 171 | $scope.$digest(); 172 | 173 | expect(spy).to.have.not.been.called; 174 | expect(eventSpy).to.have.not.been.called; 175 | expect(anotherEventSpy).to.have.been.calledOnce; 176 | }); 177 | 178 | }); 179 | 180 | describe('by event name and function', function(){ 181 | 182 | it('should remove the listener with that function', function () { 183 | var listener = $sails.on('event', spy); 184 | $sails.off('event', listener); 185 | mockIoSocket.emit('event'); 186 | $scope.$digest(); 187 | 188 | expect(spy).to.have.not.been.called; 189 | }); 190 | 191 | it('should only remove the listener with the function', function () { 192 | var eventSpy = sinon.spy() 193 | var anotherEventSpy = sinon.spy() 194 | $sails.on('event', spy); 195 | var listner = $sails.on('event', eventSpy); 196 | $sails.on('anotherEvent', anotherEventSpy); 197 | 198 | $sails.off('event', listner); 199 | mockIoSocket.emit('event'); 200 | mockIoSocket.emit('anotherEvent'); 201 | $scope.$digest(); 202 | 203 | expect(eventSpy).to.have.not.been.called; 204 | expect(spy).to.have.been.calledOnce; 205 | expect(anotherEventSpy).to.have.been.calledOnce; 206 | }); 207 | 208 | }); 209 | 210 | }); 211 | 212 | describe('once', function () { 213 | 214 | it('should apply asynchronously', function () { 215 | $sails.once('event', spy); 216 | 217 | mockIoSocket.emit('event'); 218 | 219 | expect(spy).to.have.been.not.called; 220 | $scope.$digest(); 221 | 222 | expect(spy).to.have.been.calledOnce; 223 | }); 224 | 225 | it('should only run once', function () { 226 | var counter = 0; 227 | $sails.once('event', spy); 228 | 229 | mockIoSocket.emit('event'); 230 | mockIoSocket.emit('event'); 231 | $scope.$digest(); 232 | 233 | expect(spy).to.have.been.calledOnce; 234 | }); 235 | 236 | }); 237 | 238 | methods.forEach(function(method){ 239 | 240 | describe(method, function() { 241 | 242 | it('should return a promise', function () { 243 | var promise = $sails[method]('test'); 244 | 245 | expect(promise['finally']).to.be.a('function'); 246 | expect(promise.then).to.be.a('function'); 247 | expect(promise.success).to.be.a('function'); 248 | expect(promise.error).to.be.a('function'); 249 | 250 | }); 251 | 252 | it('should return chainable success()', function () { 253 | var promise = $sails[method]('test').success(); 254 | 255 | expect(promise['finally']).to.be.a('function'); 256 | expect(promise.then).to.be.a('function'); 257 | expect(promise.success).to.be.a('function'); 258 | expect(promise.error).to.be.a('function'); 259 | 260 | }); 261 | 262 | it('should return chainable error()', function () { 263 | var promise = $sails[method]('test').error(); 264 | 265 | expect(promise['finally']).to.be.a('function'); 266 | expect(promise.then).to.be.a('function'); 267 | expect(promise.success).to.be.a('function'); 268 | expect(promise.error).to.be.a('function'); 269 | 270 | }); 271 | 272 | describe('response', function(){ 273 | 274 | var errorSpy; 275 | 276 | beforeEach(function() { 277 | mockIoSocket.on(method, function(ctx, cb){ 278 | cb(response[ctx.url]); 279 | }); 280 | errorSpy = sinon.spy(); 281 | }); 282 | 283 | it('should resolve successes', function () { 284 | 285 | $sails[method]('success').then(spy, errorSpy); 286 | $scope.$digest(); 287 | 288 | expect(errorSpy).to.have.been.not.called; 289 | expect(spy).to.have.been.calledOnce; 290 | }); 291 | 292 | it('should reject errors', function () { 293 | 294 | $sails[method]('error').then(errorSpy, spy); 295 | $scope.$digest(); 296 | 297 | expect(errorSpy).to.have.been.not.called; 298 | expect(spy).to.have.been.calledOnce; 299 | }); 300 | 301 | it('should call success for successes', function () { 302 | 303 | $sails[method]('success').success(spy).error(errorSpy); 304 | $scope.$digest(); 305 | 306 | expect(errorSpy).to.have.been.not.called; 307 | expect(spy).to.have.been.calledOnce; 308 | }); 309 | 310 | it('should call error for errors', function () { 311 | 312 | $sails[method]('error').success(errorSpy).error(spy); 313 | $scope.$digest(); 314 | 315 | expect(errorSpy).to.have.been.not.called; 316 | expect(spy).to.have.been.calledOnce; 317 | }); 318 | 319 | }); 320 | 321 | }); 322 | }); 323 | 324 | describe('modelUpdater', function() { 325 | 326 | var models; 327 | var modelResponse; 328 | var removeUpdater; 329 | 330 | beforeEach(function() { 331 | models = []; 332 | removeUpdater = $sails.$modelUpdater('user', models); 333 | modelResponse = { 334 | created:{ 335 | data: { 336 | createdAt: "2014-08-01T05:50:19.855Z", 337 | id: 1, 338 | name: "joe", 339 | updatedAt: "2014-08-01T05:50:19.855Z" 340 | }, 341 | id: 1, 342 | verb: "created" 343 | }, 344 | updated:{ 345 | data: { 346 | createdAt: "2014-08-01T05:50:19.855Z", 347 | id: 1, 348 | name: "joe Changed", 349 | updatedAt: "2014-08-01T05:51:19.855Z" 350 | }, 351 | id: 1, 352 | verb: "updated" 353 | }, 354 | destroyed:{ 355 | id: 1, 356 | verb: "destroyed" 357 | } 358 | }; 359 | }); 360 | 361 | it('should add created model to models', function () { 362 | mockIoSocket.emit('user', modelResponse.created); 363 | $scope.$digest(); 364 | expect(models).to.contain(modelResponse.created.data); 365 | }); 366 | 367 | it('should update existing model in models', function () { 368 | models.push(modelResponse.created.data); 369 | mockIoSocket.emit('user', modelResponse.updated); 370 | $scope.$digest(); 371 | expect(models[0].name).to.equal(modelResponse.updated.data.name); 372 | }); 373 | 374 | it('should remove destroyed model in models', function () { 375 | models.push(modelResponse.created.data); 376 | mockIoSocket.emit('user', modelResponse.destroyed); 377 | $scope.$digest(); 378 | expect(models).to.be.empty(); 379 | }); 380 | 381 | it('should add non-existent updated model to models based on previous', function () { 382 | modelResponse.updated.previous = modelResponse.created.data; 383 | mockIoSocket.emit('user', modelResponse.updated); 384 | $scope.$digest(); 385 | expect(models[0].name).to.equal(modelResponse.updated.data.name); 386 | }); 387 | 388 | describe('returned function', function(){ 389 | it('should remove the socket listener', function () { 390 | removeUpdater(); 391 | $scope.$digest(); 392 | mockIoSocket.emit('user', modelResponse.created); 393 | $scope.$digest(); 394 | expect(models).to.be.empty(); 395 | }); 396 | 397 | it('should remove only the socket listener it is called on', function () { 398 | var tasks = []; 399 | $sails.$modelUpdater('tasks', tasks); 400 | removeUpdater(); 401 | $scope.$digest(); 402 | mockIoSocket.emit('user', modelResponse.created); 403 | mockIoSocket.emit('tasks', modelResponse.created); 404 | $scope.$digest(); 405 | expect(tasks).to.contain(modelResponse.created.data); 406 | expect(models).to.be.empty(); 407 | }); 408 | }); 409 | 410 | }); 411 | 412 | }); 413 | --------------------------------------------------------------------------------