codeepic codeepic - 6 months ago 42
AngularJS Question

Problems with testing SignalR with Jasmine in Angular app

I have a serious issue here. My application relies on SignalR functionality, but because of that I am unable to write unit tests. I am new to testing frameworks and have used Jasmine only in simple cases. SignalR has proven to be too much of a challenge for me, but I need to understand how I can successfully test it.
This is my CommsApp.ts file [typescript]:

/// <reference path="References.ts" />
var commsAnimationsModule = angular.module('forge.communications.animations', ['ngAnimate']);
var commsDirectivesModule = angular.module('forge.communications.directives', []);
var commsServicesModule = angular.module('forge.communications.services', []);
var commsFiltersModule = angular.module('forge.communications.filters', []);

var commsApp = angular.module('forge.communications.CommsApp',
[
'ngRoute',
'ngAnimate',
'cfValidation',
'ui.bootstrap',
'forge.communications.animations',
'forge.communications.directives',
'forge.communications.services',
'forge.communications.filters',
'angularFileUpload',
'timeRelative'
]);

commsApp.config(function ($routeProvider: ng.route.IRouteProvider, $locationProvider: any) {

$locationProvider.html5Mode(true);
$routeProvider.
when('/scheduled-messages', {
templateUrl: '/forge/CommsApp/js/Controllers/ScheduledMessageList/ScheduledMessageList.html',
controller: 'ScheduledMessageListController'
}).
when('/geotriggered-messages', {
templateUrl: '/forge/CommsApp/js/Controllers/GeoMessageList/GeoMessageList.html',
controller: 'GeoMessageListController'
}).
when('/scheduled-message/create', {
templateUrl: '/forge/CommsApp/js/Controllers/CreateScheduledMessage/CreateScheduledMessage.html',
controller: 'CreateScheduledMessageController'
}).
when('/scheduled-message/edit/:id', {
templateUrl: '/forge/CommsApp/js/Controllers/EditScheduledMessage/EditScheduledMessage.html',
controller: 'EditScheduledMessageController'
}).
when('/geotriggered-message/create', {
templateUrl: '/forge/CommsApp/js/Controllers/CreateGeotriggeredMessage/CreateGeotriggeredMessage.html',
controller: 'CreateGeotriggeredMessageController'
}).
when('/geotriggered-message/edit/:id', {
templateUrl: '/forge/CommsApp/js/Controllers/EditGeotriggeredMessage/EditGeotriggeredMessage.html',
controller: 'EditGeotriggeredMessageController'
}).
otherwise({
redirectTo: '/scheduled-messages'
});
});

commsApp.run(function ($rootScope: ICommsRootScope, commsSignalrEventService: CommsSignalrEventService, commsMgmtHttpService: CommsMgmtHttpServiceClient) {

// set up the items on the root scope
$rootScope.SelectedLocale = 'en-ie';
$rootScope.ForgeApplicationKey = "9496B737-7AE2-4FBD-B271-A64160759177";
$rootScope.AppVersionString = "1.0.0";
$rootScope.SessionToken = getCookie("ForgeSessionToken");

commsSignalrEventService.initialize().done(() => {
// send any messages about a new user logging in to the application here.
});

// call this at app startup to pre-cache this data for the create and edit pages
commsMgmtHttpService.GetUpdateMessageEditorOptions();
});

function getCookie(name:string) {
var value = "; " + document.cookie;
var parts = value.split("; " + name + "=");
if (parts.length == 2) return parts.pop().split(";").shift();
}


This is the test file (I removed most of the code since it's not working anyway, I have been chasing my own tail here):

describe('forge.communications.CommsApp', function () {

beforeEach(module('forge.communications.CommsApp'));

var route, rootScope, proxy, commsSignalrEventService;

beforeEach(inject(function (_$route_, _$rootScope_, _commsSignalrEventService_) {
route = _$route_,
rootScope = _$rootScope_;
commsSignalrEventService = _commsSignalrEventService_;
}));

describe("should map routes to controllers and templates", function () {

it("/scheduled-messages route should be mapped to ScheduledMessageListController", function () {
expect(2).toEqual(2);
expect(route.routes['/scheduled-messages'].controller).toBe('ScheduledMessageListController');
});

});
});


This is CommsSignalREventService.ts file:

var servicesModule: ng.IModule = angular.module('forge.communications.services');

servicesModule.factory('commsSignalrEventService', function ($rootScope): CommsSignalrEventService {
return new CommsSignalrEventService($rootScope);
});

class CommsSignalrEventService {

private $rootScope: ng.IScope;
private proxy:any = null;

constructor($rootScope) {
this.$rootScope = $rootScope;
}

public initialize(): JQueryPromise<any>{

this.proxy = $.connection['communicationsCenterHub'];

console.log('proxy',this.proxy);

//scheduled messages
this.proxy.client.broadcastScheduledMessageCreatedEvent = (messageId: number) => {
this.$rootScope.$broadcast('comms-message-created', { messageId: messageId });
};

this.proxy.client.broadcastScheduledMessageUpdatedEvent = (messageId: number) => {
this.$rootScope.$broadcast('comms-message-updated', { messageId: messageId });
};

this.proxy.client.broadcastScheduledMessageStateChangedEvent = (messageId: number) => {
this.$rootScope.$broadcast('comms-message-statechanged', { messageId: messageId });
};

this.proxy.client.broadcastScheduledMessageDeletedEvent = (messageId: number) => {
this.$rootScope.$broadcast('comms-message-deleted', { messageId: messageId });
};

//geotriggered messages
this.proxy.client.broadcastGeoMessageCreatedEvent = (messageId: number) => {
this.$rootScope.$broadcast('comms-geomessage-created', { messageId: messageId });
};

this.proxy.client.broadcastGeoMessageUpdatedEvent = (messageId: number) => {
this.$rootScope.$broadcast('comms-geomessage-updated', { messageId: messageId });
};

this.proxy.client.broadcastGeoMessageStateChangedEvent = (messageId: number) => {
this.$rootScope.$broadcast('comms-geomessage-statechanged', { messageId: messageId });
};

this.proxy.client.broadcastGeoMessageDeletedEvent = (messageId: number) => {
this.$rootScope.$broadcast('comms-geomessage-deleted', { messageId: messageId });
};

var promise = $.connection.hub.start();
promise.done(function () {
//console.log('comms signalr hub started');
});
return promise;
}

public RegisterScheduledMessageCreated(messageId: number): void{
this.proxy.server.registerScheduledMessageCreated(messageId);
}

public RegisterScheduledMessageUpdated(messageId: number): void {
this.proxy.server.registerScheduledMessageUpdated(messageId);
}

public RegisterScheduledMessageDeleted(messageId: number): void {
this.proxy.server.registerScheduledMessageDeleted(messageId);
}

public RegisterScheduledMessageStateChanged(messageId: number): void {
this.proxy.server.registerScheduledMessageStateChanged(messageId);
}

public RegisterGeoMessageCreated(messageId: number): void {
this.proxy.server.registerGeoMessageCreated(messageId);
}

public RegisterGeoMessageUpdated(messageId: number): void {
this.proxy.server.registerGeoMessageUpdated(messageId);
}

public RegisterGeoMessageDeleted(messageId: number): void {
this.proxy.server.registerGeoMessageDeleted(messageId);
}

public RegisterGeoMessageStateChanged(messageId: number): void {
this.proxy.server.registerGeoMessageStateChanged(messageId);
}
}


The error I am seeing constantly in the command line, whenever I run karma, is forge.communications.CommsApp encountered a declaration exception FAILED
TypeError: Cannot read property 'client' of undefined at CommsSignalREventService, meaning that the 'proxy' variable in CommsSignalREventService.ts file is undefined:

this.proxy = $.connection['communicationsCenterHub'];
console.log('proxy', this.proxy); //resolves to UNDEFINED in tests, works fine in the app

this.proxy.client.broadcastScheduledMessageCreatedEvent = function (messageId) {
_this.$rootScope.$broadcast('comms-message-created', { messageId: messageId });
};


I will appreciate any help, because the amount of time I have put trying to figure it out is beyond ridiculous at this stage.

Answer

I think your issue is that your actual app references JavaScript dynamically generated by SignalR at runtime. This script is generally found at "~/signalr/hubs" or "~/signalr/js".

This script is what creates the $.connection['communicationsCenterHub'] proxy for you. Without a reference to this script in your test, this proxy will be undefined.

I'm presuming that you are not running the SignalR server when you are running your Jasmine tests. If this is the case you have two options:

  1. You can simply copy the script generated by SignalR at "~/signalr/hubs" to a file and reference that in your tests. You could manually do this by pointing your browser to the generated script URL and, but you would have to update this file anytime your hub changes. You can automate this by running signalr.exe ghp /path:[path to the .dll that contains your Hub class] as a post-build step which will generate this file for you.

  2. You can just avoid using the generated proxy altogether. Look at the "Without the generated proxy" section of the SignalR JS API reference.