Testing event chains and module.run() in Angular + Jasmine - angularjs

I am trying to test a module that has the following code:
angular.module('angularEnterpriseAuthorization').run(['$rootScope', '$state', 'AppConfig',
function($rootScope, $state, AppConfig) {
// On every time the user changes state check to see if the user has permissions to go to the new state
$rootScope.$on("$stateChangeStart", function(event, toState, toParams, fromState, fromParams) {
// If the state is not one of the public states as defined in the modules config
if (AppConfig.publicStates.indexOf(toState.name) < 0) {
event.preventDefault();
$state.go(toState, toParams, {notify: false}).then(function() {
$rootScope.$broadcast('$stateChangeSuccess', toState, toParams, fromState, fromParams);
});
}
});
]);
My test looks like this:
beforeEach(module('angularEnterpriseAuthorization', 'coreConfiguration'));
beforeEach(inject(function(_$rootScope_, _$httpBackend_, _AppConfig_) {
$scope = _$rootScope_.$new();
$httpBackend = _$httpBackend_;
AppConfig = _AppConfig_
spyOn($scope, '$broadcast').andCallThrough();
}));
it('should allow navigation to public states', function() {
$scope.$broadcast('$stateChangeStart', [{},{name:AppConfig.publicStates[0]}]);
expect($scope.$broadcast).toHaveBeenCalledWith('$stateChangeStart', [{}, {name: AppConfig.publicStates[0]}]);
$scope.$broadcast.reset();
expect($scope.$broadcast).toHaveBeenCalledWith('$stateChangeSuccess');
});
The problem I am having is that the second expect is returning false. I think the issues is that the module is not being initialized with the same $rootScope.
Any help would be appreciated!
Thanks

In your run block, you subscribe a $stateChangeStart on $rootScope and also broadcast a $stateChangeSuccess event from $rootScope.
In your test, you have to do the same, using the $rootscope. May be change this line:
$scope = _$rootScope_.$new();
to just this:
$scope = _$rootScope_;
And also you have to remove the $scope.$broadcast.reset(), that will clear all the remembered calls.
To test the second call of the same method, you could do it like this:
it('should allow navigation to public states', function() {
$scope.$broadcast('$stateChangeStart', [{},{name:AppConfig.publicStates[0]}]);
expect($scope.$broadcast).toHaveBeenCalledWith('$stateChangeStart', [{}, {name: AppConfig.publicStates[0]}]);
$scope.$apply();
expect($scope.$broadcast.calls[1].args[0]).toEqual('$stateChangeSuccess');
});
Hope this helps.

Related

Angular app.run possibility of keeping data in app

There is a possibility in app.run I record any information in this case last at $stateParams and write to the $rootScoope?
It would be simple because I pull the server a coming collection mongodb and I just editing it throughout the app.
I want to prevent in all state entries I have to pull the less data.
my app.run in test:
.run(function($rootScope, $state, $stateParams, CLIENTS) {
$rootScope.$state = $state;
$rootScope.$stateParams = $stateParams;
console.log($stateParams);
CLIENTS.getList({client: client})
.then(function(res){
console.log(res);
});
});
Follow this path:
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
// get Client data
if(!$rootScope.clientSet) {
CLIENTS.getList({client: toParams.client})
.then(function(res){
$rootScope.clientSet = res[0];
});
}
}
https://github.com/angular-ui/ui-router/wiki#state-change-events
You can listen for events so you can refresh data in rootScope.
$rootScope.$on('$stateChangeSuccess',
function(event, toState, toParams, fromState, fromParams){ ... })

Change query param in url using UI-router

Using ui-router how can I accomplish the following URL change? For example, when a user goes to "/page?hello=True" I want the url to change to "/page?status=hello".
$stateProvider.state('page', {
url: '/page/:id?status',
controller: 'MyCtrl as myCtrl',
templateProvider: function($templateCache) {
return $templateCache.get('templates/route.html');
}
});
You can use the stateChangeStart event in the app.run method to listen for state changes into that route, and change anything you want on the params. Something like this, might need to play with toState and toParams to get it right for you...
.run(['$state', '$rootScope', function($state, $rootScope) {
$rootScope.$on('$stateChangeStart', function(e, toState, toParams, fromState, fromParams) {
if (toState.name === 'invitation') {
//change toParams to whatever you want
}
});
});

ui-router test failed with event.preventDefault() in $stateChangeStart

I have problem with my angular test.
describe('Routes test', function() {
//Initialize global variables
var $state,
$location;
// Load the main application module
beforeEach(module("MyApp"));
beforeEach(inject(function(_$location_, _$state_, $templateCache) {
$state = _$state_;
$location = _$location_;
// We need add the template entry into the templateCache if we ever
// specify a templateUrl
$templateCache.put('signin.admin.view.html', 'signin');
}));
it('should be redirect to signin if user is not loggedin', function() {
$rootScope.$apply(function() {
$location.path('/');
});
expect($state.current.name).toEqual('signin');
console.log($state.current);
});
});
So I expect signin state because user is not logged in. But from console $state.current is still:
Object{name: '', url: '^', views: null, abstract: true}
To redirect I used this code:
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
if (toState.name.indexOf('signin') === 0 ) {
if( loggedIn ) {
event.preventDefault();
$state.go('layout.dashboard');
}
} else {
if( !loggedIn ) {
event.preventDefault();
$state.go('signin');
}
}
});
Variable loggedIn is false. So I dont understand, when I make test manually in browser $state.current is as expected. But not in this test.
Thank you for your help.
Rather than doing $rootScope.$apply(...) try doing:
$location.path('/');
$rootScope.$broadcast('$locationChangeSuccess');
UI Router listens to the $locationChangeSuccess event that is fired from angular's $location service. When it catches this event it will find the correct state and begin navigating to that state, hence firing the $stateChangeStart event.
Hope this helps!

Unit testing angular-ui router maximum callstack

I'm new to angular-ui-router and I've been trying to do some unit testing for a basic authentication, its working fine until I hit a maximum call stack error.
I've narrowed the error down to the $state.go call in the app.run section.
I remove this and the test works. However it breaks my app
What can I do to resolve this? so I can test this section and make it work as well?
Why does this work normally but causes a test error?
Error:
RangeError: Maximum call stack size exceeded
at Scope.$broadcast (/Users/paulrobinson/Workspace/contactCachePOC/dev/bower_components/angular/angular.js:12876:15)
at Object.transitionTo (/Users/paulrobinson/Workspace/contactCachePOC/dev/bower_components/angular-ui-router/release/angular-ui-router.js:2584:24)
at Object.go (/Users/paulrobinson/Workspace/contactCachePOC/dev/bower_components/angular-ui-router/release/angular-ui-router.js:2454:21)
at /Users/paulrobinson/Workspace/contactCachePOC/dev/js/core.js:9:5889
at Scope.$broadcast (/Users/paulrobinson/Workspace/contactCachePOC/dev/bower_components/angular/angular.js:12874:28)
at Object.transitionTo (/Users/paulrobinson/Workspace/contactCachePOC/dev/bower_components/angular-ui-router/release/angular-ui-router.js:2584:24)
at Object.go (/Users/paulrobinson/Workspace/contactCachePOC/dev/bower_components/angular-ui-router/release/angular-ui-router.js:2454:21)
at /Users/paulrobinson/Workspace/contactCachePOC/dev/js/core.js:9:5889
at Scope.$broadcast (/Users/paulrobinson/Workspace/contactCachePOC/dev/bower_components/angular/angular.js:12874:28)
at Object.transitionTo (/Users/paulrobinson/Workspace/contactCachePOC/dev/bower_components/angular-ui-router/release/angular-ui-router.js:2584:24)
Code:
app.config(['$stateProvider', '$urlRouterProvider',
function($stateProvider, $urlRouterProvider){
$urlRouterProvider.otherwise('/');
var access = routingConfig.roles;
$stateProvider
.state('start', {
url : '/',
templateUrl : 'partials/decide.html',
controller : 'decideController',
data: {
access: access.anon
}
});
}]);
app.run(['$rootScope', '$state', 'AuthService', '$log', '$location',
function ($rootScope, $state, AuthService, $log, $location) {
$rootScope.$on("$stateChangeStart",
function (event, toState, toParams, fromState, fromParams) {
if (!AuthService.isAuthorized(toState.data.access)) {
event.preventDefault();
$rootScope.error = null;
//STATE.GO is causing the error
$state.go('start');
//$location.path('/#/');
return;
}
});
}]);
describe('Test as an anonymous user', function () {
var $templateCache, $state, $stateParams, $rootScope, $httpBackend,
AuthService,, $location;
var roles = {
anon: { id: 0, value: 'Public'},
user: { id: 1, value: 'User'}
};
beforeEach(module('app'));
beforeEach(inject(function(_$templateCache_, _$state_, _$stateParams_, _$rootScope_, _$httpBackend_,
_AuthService_, _sessionService_, _$location_) {
$templateCache = _$templateCache_;
$state = _$state_;
$stateParams = _$stateParams_;
$rootScope = _$rootScope_;
$httpBackend = _$httpBackend_;
AuthService = _AuthService_;
$location = _$location_;
//Fake it and say we're not authorized.
spyOn(AuthService, "isAuthorized").andCallFake(function (state){
return false;
});
}));
describe('View page.', function () {
beforeEach(function () {
$state.go('start', { });
$rootScope.$apply();
});
it('Should view page.', function () {
expect($state.current.name).toEqual('start');
});
});
});
Maximum call stack exceeded suggests that something is happening recursively. I'd guess that the state that you are going to "start" is also not authorised, so you get redirected to "start" again, and so on.
You need to make your AuthService.IsAuthorized return true at some point
Ok so it appears to be a bug with ui-router.
Here is a solution
https://github.com/angular-ui/ui-router/issues/178#issuecomment-49156829
$state.go(toState.name, toParams, {notify: false}).then(function() {
$rootScope.$broadcast('$stateChangeSuccess', toState, toParams, fromState, fromParams);
});
The above code will deal with the problem without breaking the browser or unit tests.
Just beware of https://github.com/angular-ui/ui-router/issues/1348 when using this fix.
Use $state.go("main", {}, {notify:false}); for notify to $stateChangeStart event.
$state.go("main", {}, {notify:false});

karma: unit testing controller that listens on angular-ui-router event

I try to unit test an angularJS controller with Kara/Sinon/Jasmine.
The controller listens to state changes from angular-ui-router to call a service.
I stubbed the service method (update) using sinon and i use $broadcast to throw the event in the tests.
I use $apply to fire the event.
However, it seems the controller does not seem to react.
I try to check for teh sinon stubbed method and i also use console.log to see weather or not the controller reacts - but it does not.
It does in the manual browser test, so i guess it is my test that needs to be fixed (should be the other way round, i know).
What am i doing wrong here?
the karma test specs:
'use strict';
describe('controllers', function() {
beforeEach(module('myApp.controllers'));
beforeEach(module('myApp.services'));
beforeEach(module('ui.router'));
describe('myAppController', function() {
var scope, ctrl;
beforeEach(inject(function(
$rootScope, $controller, $injector,ParamsHelper) {
scope = $rootScope.$new();
scope.ParamsHelperStub = sinon.stub(ParamsHelper, 'update');
ctrl = $controller('myAppController', {
$scope: scope,
ParamsHelper: scope.ParamsHelperStub
});
}));
it('should use ParamsHelper if is deepLink', function() {
var event = {};
var toState = {name:'regions'};
var toParams = ['test'];
scope.AppSettingsStub.state.isDeepLink = true;
scope.$broadcast("$stateChangeStart", event, toState, toParams);
scope.$apply();
expect(scope.ParamsHelperStub.calledWith(['test'])).toBeTruthy();
});
});
});
From the controller:
...
rootScope.$on('$stateChangeStart',
function(event, toState, toParams) {
console.log('VibeController: $stateChangeStart Event catched');
ParamsHelper.update(toParams);
...
The problem was in the scope.
To fire an event that can be recognized by the controller i need to fire it in the rootScope context.
I added the rootScope in the initial injection and then edited the broadcasting command accordingly:
describe('myAppController', function() {
var scope, ctrl, rootScope;
beforeEach(inject(function(
$rootScope, $controller, $injector, ParamsHelper) {
rootScope = $rootScope;
...
rootCope.$broadcast("$stateChangeStart", event, toState, toParams);

Resources