├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jshintrc ├── .travis.yml ├── Gruntfile.js ├── LICENSE ├── README.md ├── karma.conf.js ├── package.json ├── src └── angular-signalr.js └── test ├── angular-signalr_spec.js └── mock └── signalr-hub.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.html eol=lf 2 | *.css eol=lf 3 | *.js eol=lf 4 | *.md eol=lf 5 | *.json eol=lf 6 | *.yml eol=lf 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # npm 2 | node_modules 3 | 4 | # bower 5 | bower_components 6 | 7 | # testing 8 | coverage 9 | 10 | # IntelliJ project settings 11 | .idea 12 | *.iml 13 | 14 | # others 15 | pids 16 | logs 17 | results 18 | dist 19 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "eqeqeq": true, 4 | "curly": true, 5 | "strict": true, 6 | "trailing": true, 7 | "white": true, 8 | "undef": true, 9 | "unused": false, 10 | "indent": 2, 11 | "quotmark": "single", 12 | 13 | "eqnull": true, 14 | "expr": true, 15 | "newcap": false, 16 | "loopfunc": true, 17 | "bitwise": false, 18 | 19 | "browser": true, 20 | "devel": true, 21 | "predef": [ 22 | "angular", 23 | "expect", 24 | "describe" 25 | ], 26 | "globals": { 27 | "angular": false, 28 | 29 | // For Jasmine 30 | "after" : false, 31 | "afterEach" : false, 32 | "before" : false, 33 | "beforeEach" : false, 34 | "describe" : false, 35 | "expect" : false, 36 | "jasmine" : false, 37 | "module" : false, 38 | "spyOn" : false, 39 | "inject" : false, 40 | "it" : false, 41 | "xdescribe" : false, 42 | "xit" : false 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | 5 | before_install: 6 | - 'export DISPLAY=:99.0' 7 | - 'sh -e /etc/init.d/xvfb start' 8 | 9 | before_script: 10 | - 'npm install -g bower grunt-cli' 11 | - 'npm install -g codeclimate-test-reporter' 12 | 13 | script: 14 | - 'grunt' 15 | 16 | after_success: 17 | - 'grunt coverage' 18 | 19 | after_script: 20 | - codeclimate < coverage/**/lcov.info 21 | 22 | addons: 23 | code_climate: 24 | repo_token: 28410a5a1957cc8e8575610c2f83c05f454e0386aa27a9dc8d97f970d99f784b 25 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (grunt) { 4 | 5 | 6 | grunt.loadNpmTasks('grunt-karma'); 7 | grunt.loadNpmTasks('grunt-karma-coveralls'); 8 | grunt.loadNpmTasks('grunt-ddescribe-iit'); 9 | grunt.loadNpmTasks('grunt-contrib-jshint'); 10 | grunt.loadNpmTasks('grunt-contrib-uglify'); 11 | grunt.loadNpmTasks('grunt-contrib-watch'); 12 | 13 | 14 | grunt.initConfig({ 15 | 16 | //user-defined configs 17 | dist: 'dist', 18 | 19 | // Karma setup, a place to override the default options set in karma.config.js 20 | // 21 | karma: { 22 | //global karma options 23 | options: { 24 | configFile: 'karma.conf.js' 25 | }, 26 | watch: { 27 | background: true 28 | }, 29 | continuous: { 30 | singleRun: false 31 | }, 32 | travis: { 33 | singleRun: true, 34 | browsers: ['Firefox'], 35 | preprocessors: { 36 | 'src/**/*.js': 'coverage' 37 | }, 38 | reporters: ['progress', 'coverage'], 39 | coverageReporter: { 40 | type: 'lcov', // lcov or lcovonly are required for generating lcov.info files 41 | dir: 'coverage/' 42 | } 43 | } 44 | }, 45 | 46 | 47 | // Sends coverage report to Coveralls 48 | // 49 | coveralls: { 50 | options: { 51 | debug: true, 52 | coverageDir: 'coverage' 53 | } 54 | }, 55 | 56 | 57 | 'ddescribe-iit': { 58 | files: [ 59 | 'test/**/*.spec.js' 60 | ] 61 | }, 62 | 63 | 64 | jshint: { 65 | options: { 66 | jshintrc: '.jshintrc' 67 | }, 68 | gruntfile: 'Gruntfile.js', 69 | src: 'src/**/*.js', 70 | test: 'test/*.js' 71 | }, 72 | 73 | 74 | // Minifies the JavaScript code 75 | // 76 | uglify: { 77 | dist: { 78 | files: { 79 | '<%= dist %>/angular-signalr.min.js': ['src/angular-signalr.js'] 80 | } 81 | } 82 | }, 83 | 84 | 85 | delta: { 86 | js: { 87 | files: ['src/**/*.js', 'test/**/*.js'], 88 | //we don't need to jshint here, it slows down everything else 89 | tasks: ['karma:watch:run'] 90 | } 91 | } 92 | 93 | 94 | }); 95 | 96 | 97 | // generate coverage report and send it to coveralls 98 | grunt.registerTask('coverage', ['karma:travis', 'coveralls']); 99 | 100 | 101 | //register before and after test tasks so we've don't have to change cli 102 | //options on the google's CI server 103 | grunt.registerTask('before-test', ['ddescribe-iit', 'jshint']); 104 | grunt.registerTask('build', ['uglify']); 105 | grunt.registerTask('after-test', ['build']); 106 | 107 | grunt.registerTask('test', 'Run tests on singleRun karma server', function () { 108 | if (process.env.TRAVIS) { 109 | grunt.task.run('karma:travis'); 110 | } else { 111 | grunt.task.run('karma:continuous'); 112 | } 113 | }); 114 | 115 | 116 | 117 | 118 | 119 | //Rename our watch task to 'delta', then make actual 'watch' 120 | //task build things, then start test server 121 | grunt.renameTask('watch', 'delta'); 122 | 123 | 124 | 125 | grunt.registerTask('watch', ['before-test', 'after-test', 'karma:watch', 'delta']); 126 | // Default task. 127 | grunt.registerTask('default', ['before-test', 'test', 'after-test']); 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | //// default task 136 | //grunt.registerTask('default', ['jshint', ' :unit', 'build']); 137 | // 138 | //// automatically execute the unit tests when a file changes 139 | //grunt.registerTask('watch', ['karma:watch']); 140 | // 141 | //// generate coverage report and send it to coveralls 142 | //grunt.registerTask('coverage', ['karma:coverage', 'coveralls']); 143 | // 144 | //// generate combined and minified distribution files 145 | //grunt.registerTask('build', ['concat', 'uglify']); 146 | 147 | 148 | }; 149 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Roy Lee 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 | 2 | # angular-signalr 3 | [![Build Status](https://travis-ci.org/roylee0704/angular-signalr.svg?branch=master)](https://travis-ci.org/roylee0704/angular-signalr) 4 | [![Code Climate](https://codeclimate.com/github/roylee0704/angular-signalr/badges/gpa.svg)](https://codeclimate.com/github/roylee0704/angular-signalr) 5 | [![Coverage Status](https://coveralls.io/repos/roylee0704/angular-signalr/badge.svg?branch=master)](https://coveralls.io/r/roylee0704/angular-signalr?branch=master) 6 | [![Dependency Status](https://gemnasium.com/roylee0704/angular-signalr.svg)](https://gemnasium.com/roylee0704/angular-signalr) 7 | 8 | 9 | Bower Component for using AngularJS with [SignalR.NET](http://signalr.net/). 10 | 11 | 12 | ## Install 13 | 14 | 1. `bower install angular-signalr` or [download the zip](https://github.com/roylee0704/angular-signalr/archive/master.zip). 15 | 2. Make sure the SignalR.NET client lib is loaded. 16 | 3. Include the `jquery.signalR.min.js` script provided by this component into your app. 17 | 4. Add `roy.signalr-hub` as a module dependency to your app. 18 | 19 | 20 | ## Usage 21 | 22 | This module exposes a `hubFactory`, which is an API for instantiating 23 | signalr-hubs that are integrated with Angular's digest cycle. 24 | 25 | 26 | 27 | ### Making a Hub Instance 28 | 29 | ```javascript 30 | // in the top-level module of the app 31 | angular.module('myApp', [ 32 | 'roy.signalr-hub', 33 | 'myApp.MyCtrl' 34 | ]). 35 | factory('myHub', function (hubFactory) { 36 | return hubFactory('yourHubName'); 37 | }); 38 | ``` 39 | 40 | With that, you can inject your `myHub` service into controllers and 41 | other serivices within your application! 42 | 43 | ## API 44 | 45 | For the most part, this component works exactly like you would expect. The only API 46 | addition is `hub.forward`, which makes it easier to add/remove listener in a way that 47 | works with [AngularJS's scope](https://docs.angularjs.org/api/ng/type/$rootScope.Scope) 48 | 49 | ### `hub.on` 50 | Takes an event name and callback. 51 | Works just like the method of the same name from SignalR.NET. 52 | 53 | ### `hub.off` 54 | Takes an event name and callback. 55 | Works just like the method of the same name from SignalR.NET. 56 | 57 | ### `hub.invoke` 58 | Sends a message to the server. 59 | Works just like the method of the same name from SignalR.NET. 60 | 61 | ### `hub.forward` 62 | 63 | `hub.forward` allows you to forward the events received by SignalR.NET's hub to 64 | AngularJS's event system. You can then listen to the event with `$scope.on`. By default, 65 | hub-forwarded events are namespaced with `hub:`. 66 | 67 | The first argument is a string or array of strings listing the event names to be forwarded. 68 | The second argument is optional, and is the scope on which the event are to be broadcasted. If 69 | an argument is not provided, it defaults to `$rootScope`. As a reminder, broadcasted events are 70 | propagated down to descendant scopes. 71 | 72 | #### Example 73 | 74 | An easy way to make hub events available across your app: 75 | 76 | ```javascript 77 | //in the top level module of the app 78 | angular.module('myApp', [ 79 | 'roy.signalr-hub', 80 | 'myApp.MyCtrl' 81 | ]). 82 | factory('myHub', function(hubFactory) { 83 | var myHub = hubFactory(); 84 | myHub.forward('interesting-event'); 85 | return myHub; 86 | }); 87 | 88 | angular.module('myApp.MyCtrl', []). 89 | controller('MyCtrl', function() { 90 | $scope.$on('hub:interesting-event', function(ev, data) { 91 | 92 | }); 93 | }); 94 | 95 | ``` 96 | 97 | 98 | 99 | 100 | ### `hubFactory(hubName, { hub: }}` 101 | 102 | The first argument: `hubName` is a required parameter, it specifies the name of the Hub that you have created on the Server. 103 | 104 | For next argument is optional, this option allows you to provide the `hub` service with a `SignalR.NET hub` object to be used internally. 105 | This is useful if you want to connect on a different path, or need to hold a reference to the `SignalR.NET hub` object for use elsewhere. 106 | 107 | #### Example 108 | 109 | ```javascript 110 | angular.module('myApp', [ 111 | 'roy.signalr-hub' 112 | ]). 113 | factory('myHub', function (hubFactory) { 114 | var mySpecialHub = $.hubConnection('/some/path', {useDefaultPath: false}); 115 | 116 | myHub = hubFactory('yourHubName', { 117 | hub: mySpecialHub 118 | }); 119 | 120 | return myHub; 121 | }); 122 | ``` 123 | 124 | ### `hub.error` 125 | A handler for error events. 126 | Works just like the method of the same name from SignalR.NET. 127 | #### Example 128 | 129 | ```javascript 130 | angular.module('myApp', [ 131 | 'roy.signalr-hub' 132 | ]). 133 | factory('myHub', function (hubFactory) { 134 | myHub = hubFactory('yourHubName'); 135 | 136 | return myHub; 137 | }). 138 | controller('MyCtrl', function (myHub) { 139 | myHub.error(function (error) { 140 | console.log('SignalR error: ' + error) 141 | }); 142 | }); 143 | ``` 144 | 145 | 146 | ### `hub.stateChanged` 147 | Raised when the connection state changes. 148 | Works just like the method of the same name from SignalR.NET. 149 | 150 | #### Example 151 | 152 | ```javascript 153 | angular.module('myApp', [ 154 | 'roy.signalr-hub' 155 | ]). 156 | factory('myHub', function (hubFactory) { 157 | myHub = hubFactory('yourHubName'); 158 | 159 | return myHub; 160 | }). 161 | controller('MyCtrl', function (myHub) { 162 | myHub.stateChanged(function(state){ 163 | switch (state.newState) { 164 | case $.signalR.connectionState.connecting: 165 | //your code here 166 | break; 167 | case $.signalR.connectionState.connected: 168 | //your code here 169 | break; 170 | case $.signalR.connectionState.reconnecting: 171 | //your code here 172 | break; 173 | case $.signalR.connectionState.disconnected: 174 | //your code here 175 | break; 176 | } 177 | }); 178 | }); 179 | ``` 180 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Contents of: test/karma.conf.js 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | 8 | // list of files / patterns to load in the browser 9 | files: [ 10 | // libraries 11 | 'node_modules/angular/angular.js', 12 | 'node_modules/angular-mocks/angular-mocks.js', 13 | 14 | //the mock 15 | 'test/mock/*.js', 16 | 17 | // the directives 18 | 'src/**/*.js', 19 | 20 | // test 21 | 'test/*_spec.js' 22 | ], 23 | 24 | // enable / disable watching file and executing tests whenever any file changes 25 | autoWatch: false, 26 | 27 | // Continuous Integration mode 28 | // if true, Karma captures browsers, runs the tests and exits 29 | singleRun: false, 30 | 31 | // start these browsers 32 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 33 | browsers: ['Chrome'], 34 | 35 | // enable / disable colors in the output (reporters and logs) 36 | colors: true, 37 | 38 | // frameworks to use 39 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 40 | frameworks: ['jasmine'], 41 | 42 | // preprocess matching files before serving them to the browser 43 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 44 | preprocessors: {}, 45 | 46 | plugins: ['karma-*'], 47 | 48 | // test results reporter to use 49 | // possible values: 'dots', 'progress', 'junit' 50 | reporters: ['progress'] 51 | 52 | 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-signalr", 3 | "version": "0.3.2", 4 | "description": "Angular wrapper for signalr in socket.io style", 5 | "main": "angular-signalr.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/roylee0704/angular-signalr.git" 9 | }, 10 | "keywords": [ 11 | "angular", 12 | "signalr" 13 | ], 14 | "author": "Roy Lee", 15 | "license": "MIT", 16 | "readmeFilename": "README.md", 17 | "homepage": "", 18 | "bugs": { 19 | "url": "https://github.com/roylee0704/angular-signalr/issues" 20 | }, 21 | "devDependencies": { 22 | "angular": "^1.2.x", 23 | "angular-mocks": "^1.3.15", 24 | 25 | "grunt": "~0.4", 26 | "grunt-contrib-concat": "^0.5.0", 27 | "grunt-contrib-copy": "^0.8.0", 28 | "grunt-contrib-jshint": "~0.10", 29 | "grunt-contrib-uglify": "^0.7.0", 30 | "grunt-contrib-watch": "^0.6.1", 31 | "grunt-ddescribe-iit": "0.0.6", 32 | 33 | "grunt-karma": "~0.9", 34 | "grunt-karma-coveralls": "~2.5", 35 | 36 | "karma": "~0.12", 37 | "karma-jasmine": "~0.2.0", 38 | "karma-chrome-launcher": "~0.1", 39 | "karma-firefox-launcher": "~0.1", 40 | "karma-coverage": "^0.2.7" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/angular-signalr.js: -------------------------------------------------------------------------------- 1 | angular.module('roy.signalr-hub', []). 2 | constant('$', window.jQuery). 3 | provider('hubFactory', function() { 4 | 'use strict'; 5 | 6 | this.$get = ['$rootScope', '$timeout', '$', function($rootScope, $timeout, $) { 7 | 8 | var asyncAngularify = function(context, fn) { 9 | 10 | return (typeof fn === 'function')? function() { 11 | var args = arguments; 12 | $timeout(function(){ 13 | fn.apply(context, args); 14 | }, 0); 15 | }: angular.noop; 16 | 17 | }; 18 | 19 | return function hubFactory (hubName, opts) { 20 | opts = angular.extend({ 21 | hub: $.hubConnection(), 22 | logging: false, 23 | qs: {}, 24 | prefix: 'hub:' 25 | }, opts); 26 | 27 | var _hub = opts.hub; 28 | var _logging = opts.logging; 29 | var _qs = opts.qs; 30 | var _proxy = _hub.createHubProxy(hubName); 31 | var _prefix = opts.prefix; 32 | 33 | var wrappedHub = { 34 | hub: _hub, 35 | proxy: _proxy, 36 | 37 | connect: function(transObj) { 38 | _hub.logging = _logging; 39 | _hub.qs = _qs; 40 | return _hub.start(transObj); 41 | }, 42 | 43 | disconnect: function() { 44 | _hub.stop(); 45 | }, 46 | 47 | on: function(ev, fn) { 48 | _proxy.on(ev, fn.__ng = asyncAngularify(_proxy, fn)); 49 | }, 50 | 51 | off: function(ev, fn) { 52 | if(fn && fn.__ng) { 53 | fn = fn.__ng; 54 | } 55 | _proxy.off(ev, fn); 56 | }, 57 | 58 | invoke: function(ev, data) { 59 | var args = arguments; 60 | return promisify(function() { 61 | _proxy.invoke.apply(_proxy, args); 62 | }); 63 | }, 64 | 65 | stateChanged: function(fn) { 66 | return promisify(function() { 67 | _hub.stateChanged(asyncAngularify(_hub, fn)); 68 | }); 69 | }, 70 | 71 | error: function(fn) { 72 | _hub.error(asyncAngularify(_hub, fn)); 73 | }, 74 | 75 | forward: function(events, scope) { 76 | 77 | if(!scope) { 78 | scope = $rootScope; 79 | } 80 | 81 | if(events instanceof Array === false) { 82 | events = [events]; 83 | } 84 | 85 | events.forEach(function(ev) { 86 | var forwardBroadcast = asyncAngularify(_proxy, function() { 87 | Array.prototype.unshift.call(arguments, _prefix + ev); 88 | scope.$broadcast.apply(scope, arguments); 89 | }); 90 | 91 | _proxy.on(ev, forwardBroadcast); 92 | 93 | scope.$on('$destroy', function() { 94 | _proxy.off(ev, forwardBroadcast); 95 | }); 96 | }); 97 | 98 | } 99 | }; 100 | 101 | //auto-establish connection with server 102 | wrappedHub.promise = wrappedHub.connect(); 103 | 104 | var promisify = function (fn) { 105 | return wrappedHub.promise.then(function(){ 106 | return fn(); 107 | }); 108 | }; 109 | 110 | return wrappedHub; 111 | }; 112 | }]; 113 | }); 114 | -------------------------------------------------------------------------------- /test/angular-signalr_spec.js: -------------------------------------------------------------------------------- 1 | describe('socketFactory', function() { 2 | 'use strict'; 3 | 4 | beforeEach(module('roy.signalr-hub')); 5 | 6 | var hub, 7 | mockedHub, /*server hub*/ 8 | spy, 9 | $timeout, 10 | $browser, 11 | scope; 12 | 13 | beforeEach(inject(function(hubFactory, _$timeout_, $q, _$browser_, $rootScope) { 14 | 15 | mockedHub = window.jQuery.hubConnection(); 16 | spy = jasmine.createSpy('mockedFn'); 17 | $timeout = _$timeout_; 18 | $browser = _$browser_; 19 | scope = $rootScope; 20 | 21 | //setup that #connect returns a promise. 22 | spyOn(mockedHub, 'start').and.callFake(function() { 23 | var deferred = $q.defer(); 24 | deferred.resolve('resolved # connect call'); 25 | return deferred.promise; 26 | }); 27 | 28 | hub = hubFactory('testHub', { 29 | hub: mockedHub 30 | }); 31 | 32 | mockedHub.proxy = hub.proxy; 33 | 34 | })); 35 | 36 | describe('# on', function() { 37 | 38 | it('should apply asynchronously', function () { 39 | hub.on('event', spy); 40 | mockedHub.proxy.invoke('event'); 41 | 42 | expect(spy).not.toHaveBeenCalled(); 43 | 44 | $timeout.flush(); 45 | expect(spy).toHaveBeenCalled(); 46 | }); 47 | 48 | }); 49 | 50 | describe('# off', function() { 51 | 52 | it('should not call after removing an event', function () { 53 | hub.on('event', spy); 54 | hub.off('event'); 55 | 56 | mockedHub.proxy.invoke('event'); 57 | 58 | expect($browser.deferredFns.length).toBe(0); 59 | }); 60 | 61 | 62 | it('should not call after removing an event\'s watcher', function () { 63 | 64 | var spy2 = jasmine.createSpy('mockedFn2'); 65 | 66 | hub.on('event', spy); 67 | hub.on('event', spy2); 68 | 69 | hub.off('event', spy); 70 | mockedHub.proxy.invoke('event'); 71 | $timeout.flush(); 72 | 73 | expect(spy2).toHaveBeenCalled(); 74 | expect(spy).not.toHaveBeenCalled(); 75 | 76 | }); 77 | 78 | }); 79 | 80 | describe('# invoke', function() { 81 | 82 | beforeEach(function() { 83 | spyOn(mockedHub.proxy, 'invoke'); 84 | }); 85 | 86 | it('should not call the delegate hub\'s invoke prior #connect', function() { 87 | hub.invoke('event', {foo: 'bar'}); 88 | expect(mockedHub.proxy.invoke).not.toHaveBeenCalled(); 89 | 90 | //assume connection has been established. 91 | $timeout.flush(); 92 | expect(mockedHub.proxy.invoke).toHaveBeenCalled(); 93 | }); 94 | 95 | it('should allow multiple data arguments', function() { 96 | hub.invoke('event', 'x', 'y'); 97 | 98 | $timeout.flush(); 99 | expect(mockedHub.proxy.invoke).toHaveBeenCalledWith('event', 'x', 'y'); 100 | }); 101 | 102 | }); 103 | 104 | describe('# connect', function() { 105 | 106 | it('should call the delegate hub\'s #start', function() { 107 | hub.connect(); 108 | 109 | expect(mockedHub.start).toHaveBeenCalled(); 110 | }); 111 | 112 | xit('should call the delegate hub\'s #start with transport option', function() { 113 | var transObj = { 114 | transport: 'longPolling' 115 | }; 116 | 117 | hub.connect(transObj); 118 | console.log(mockedHub.start.calls.first()); 119 | expect(mockedHub.start.calls.first().args[0]).toEqual(transObj); 120 | }); 121 | 122 | it('should return a promise', function() { 123 | hub.connect().then(function() { 124 | console.log('Success'); 125 | }); 126 | 127 | $timeout.flush(); 128 | }); 129 | 130 | }); 131 | 132 | describe('# disconnect', function () { 133 | 134 | it('should call the delegate hub\'s #stop', function() { 135 | spyOn(mockedHub, 'stop'); 136 | hub.disconnect(); 137 | 138 | expect(mockedHub.stop).toHaveBeenCalled(); 139 | }); 140 | 141 | }); 142 | 143 | describe('# error', function () { 144 | 145 | beforeEach(function() { 146 | spyOn(mockedHub, 'error'); 147 | hub.error(spy); 148 | }); 149 | 150 | it('should call the delegate hub\'s #error', function() { 151 | expect(mockedHub.error).toHaveBeenCalled(); 152 | }); 153 | 154 | it('should apply asynchronously', function() { 155 | expect(mockedHub.error.calls.first().args[0]).not.toBe(spy); 156 | 157 | var error = {error : 'test-error'}; 158 | 159 | mockedHub.error.calls.first().args[0](error); 160 | expect(spy).not.toHaveBeenCalled(); 161 | 162 | $timeout.flush(); 163 | expect(spy).toHaveBeenCalledWith(error); 164 | }); 165 | 166 | }); 167 | 168 | describe('# stateChanged', function() { 169 | 170 | beforeEach(function() { 171 | spyOn(mockedHub, 'stateChanged'); 172 | hub.stateChanged(spy); 173 | }); 174 | 175 | it('should not call the delegate hub\'s stateChanged prior #connect', function() { 176 | expect(mockedHub.stateChanged).not.toHaveBeenCalled(); 177 | 178 | //assume connection has been established. 179 | $timeout.flush(); 180 | expect(mockedHub.stateChanged).toHaveBeenCalled(); 181 | }); 182 | 183 | it('should apply asynchronously', function() { 184 | 185 | //assume connection has been established. 186 | $timeout.flush(); 187 | expect(mockedHub.stateChanged.calls.first().args[0]).not.toBe(spy); 188 | 189 | var state = {state : 'test-state'}; 190 | 191 | mockedHub.stateChanged.calls.first().args[0](state); 192 | expect(spy).not.toHaveBeenCalled(); 193 | 194 | $timeout.flush(); 195 | expect(spy).toHaveBeenCalledWith(state); 196 | }); 197 | 198 | }); 199 | 200 | describe('# forward', function() { 201 | 202 | it('should forward events', function() { 203 | hub.forward('event'); 204 | 205 | scope.$on('hub:event', spy); 206 | 207 | mockedHub.proxy.invoke('event'); 208 | $timeout.flush(); 209 | expect(spy).toHaveBeenCalled(); 210 | }); 211 | 212 | it('should forward an array of events', function() { 213 | hub.forward(['e1', 'e2']); 214 | 215 | scope.$on('hub:e1', spy); 216 | scope.$on('hub:e2', spy); 217 | 218 | mockedHub.proxy.invoke('e1'); 219 | mockedHub.proxy.invoke('e2'); 220 | $timeout.flush(); 221 | 222 | expect(spy.calls.count()).toBe(2); 223 | }); 224 | 225 | it('should not fire the scope:watchers when the scope is removed', function() { 226 | 227 | hub.forward('event'); 228 | scope.$on('hub:event', spy); 229 | mockedHub.proxy.invoke('event'); 230 | $timeout.flush(); 231 | 232 | expect(spy).toHaveBeenCalled(); 233 | 234 | scope.$destroy(); 235 | spy.calls.reset(); 236 | mockedHub.proxy.invoke('event'); 237 | 238 | expect(spy).not.toHaveBeenCalled(); 239 | 240 | }); 241 | 242 | it('should cleanup scope:watchers when the scope is removed', function(){ 243 | 244 | hub.forward('event'); 245 | scope.$on('hub:event', spy); 246 | mockedHub.proxy.invoke('event'); 247 | $timeout.flush(); 248 | expect(spy).toHaveBeenCalled(); 249 | 250 | scope.$destroy(); 251 | spy.calls.reset(); 252 | 253 | //to try accessing a registered event. 254 | hub.on('event', spy); 255 | mockedHub.proxy.invoke('event'); 256 | expect(spy).not.toHaveBeenCalled(); 257 | 258 | }); 259 | 260 | it('should forward to the specified scope when one is provided', function() { 261 | 262 | var child = scope.$new(); 263 | spyOn(child, '$broadcast'); 264 | 265 | hub.forward('event', child); 266 | child.$on('hub:event', spy); 267 | 268 | mockedHub.proxy.invoke('event'); 269 | $timeout.flush(); 270 | 271 | expect(child.$broadcast).toHaveBeenCalled(); 272 | 273 | }); 274 | 275 | 276 | it('should use the specified prefix', inject(function(hubFactory) { 277 | 278 | hub = hubFactory('testHub', { 279 | hub: mockedHub, 280 | prefix: 'custom:' 281 | }); 282 | mockedHub.proxy = hub.proxy; 283 | 284 | hub.forward('event'); 285 | scope.$on('custom:event', spy); 286 | 287 | mockedHub.proxy.invoke('event'); 288 | $timeout.flush(); 289 | expect(spy).toHaveBeenCalled(); 290 | 291 | })); 292 | 293 | it('should use the specified empty prefix', inject(function(hubFactory){ 294 | 295 | hub = hubFactory('testHub', { 296 | hub: mockedHub, 297 | prefix: '' 298 | }); 299 | mockedHub.proxy = hub.proxy; 300 | 301 | hub.forward('event'); 302 | scope.$on('event', spy); 303 | 304 | mockedHub.proxy.invoke('event'); 305 | $timeout.flush(); 306 | expect(spy).toHaveBeenCalled(); 307 | 308 | })); 309 | 310 | it('should pass all arguments to scope.$on', function() { 311 | 312 | hub.forward('event'); 313 | scope.$on('hub:event', spy); 314 | mockedHub.proxy.invoke('event', 'a', 'b', 'c'); 315 | $timeout.flush(); 316 | 317 | expect(spy.calls.mostRecent().args.slice(1)).toEqual(['a', 'b', 'c']); 318 | 319 | }); 320 | 321 | }); 322 | 323 | }); 324 | -------------------------------------------------------------------------------- /test/mock/signalr-hub.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by roylee on 18/05/2015. 3 | */ 4 | 5 | var jQuery = { 6 | hubConnection: createMockHubObject 7 | }; 8 | 9 | function createMockHubObject () { 10 | 'use strict'; 11 | 12 | var connection = { 13 | createHubProxy: function (hubName) { 14 | return new HubProxy(hubName); 15 | }, 16 | start: function(transObj) {}, 17 | stop: function() {}, 18 | error: function(fn) {}, 19 | stateChanged: function(fn) {} 20 | }; 21 | 22 | return connection; 23 | } 24 | 25 | 26 | function HubProxy ( hubName ) { 27 | 'use strict'; 28 | 29 | this._listeners = {}; 30 | 31 | this.on = function (ev, fn) { 32 | (this._listeners[ev] = this._listeners[ev] || []).push(fn); 33 | }; 34 | 35 | this.off = function (ev, fn) { 36 | 37 | if(fn) { 38 | var index = this._listeners[ev].indexOf(fn); 39 | if(index > -1) { 40 | this._listeners[ev].splice(index, 1); 41 | } 42 | } else { 43 | delete this._listeners[ev]; 44 | } 45 | 46 | }; 47 | 48 | this.invoke = function (ev, data) { 49 | var listeners = this._listeners[ev]; 50 | if(listeners) { 51 | var args = arguments; 52 | listeners.forEach(function(listener){ 53 | listener.apply(null, Array.prototype.slice.call(args, 1)); 54 | }); 55 | } 56 | }; 57 | } 58 | 59 | 60 | --------------------------------------------------------------------------------