angularJS $stateProvider : How to unit test different views in UI-Router? - angularjs

How can I unit test different views for the below scenario
.state('app.sr.product.upload', {
name: 'upload',
url: '/upload',
data: {
tags: [],
userCommunities: []
},
views: {
"productView#app.sr.product": {
templateUrl: 'views/upload/upload.html',
controller: 'UploadCtrl',
controllerAs: 'ul'
},
"tags#app.sr.product.upload": {
templateUrl: 'views/tags/tags.html',
controller: 'TagsCtrl',
controllerAs: 'vm'
},
"UserCommunityPanel#app.sr.product.upload": {
templateUrl: 'views/user-community/user-community.html',
controller: 'UserCommunityCtrl',
controllerAs: 'ul'
},
}
})
If my view is tags#app.sr.product.upload then how can I test that
my controller is TagsCtrl, my controllerAs value is vm etc??
How can I unit test if my state is app.sr.product.upload then
data.tags=[], data.userCommunities=[] etc.
I searched for lot of docs and tutorials but didnt get it .
Any help is appreciable.
Thanks

Try this on for size. I'm assuming you would be using jasmine for your tests, but the concept is the same for any testing framework.
When you run your test, first subscribe to the '$stateChangeSuccess' event and then navigate to that state. Once the event fires, check the toState values to see if they are what you expect them to be.
You can run the snippet to see the tests in action.
//write a unit test
describe('state changes', function() {
beforeEach(module('app'));
var $rootScope, $state;
beforeEach(inject(function(_$rootScope_, _$state_) {
// The injector unwraps the underscores (_) from around the parameter names when matching
$rootScope = _$rootScope_;
$state = _$state_;
}));
it('loads page 1', function(done) {
//wait for the state to change, then make sure we changed to the correct state
$rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams) {
expect(toState.controller).toEqual('Controller1');
done();
});
//navigate to the state
$state.go('state1');
//start a digest cycle so ui-router will navigate
$rootScope.$apply();
});
it('loads page 2', function(done) {
//wait for the state to change, then make sure we changed to the correct state
$rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams) {
expect(toState.controller).toEqual('Controller2');
done();
});
//navigate to the state
$state.go('state2');
//start a digest cycle so ui-router will navigate
$rootScope.$apply();
});
it('loads page 3', function(done) {
//wait for the state to change, then make sure we changed to the correct state
$rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams) {
expect(toState.controller).toEqual('Controller3');
done();
});
//navigate to the state
$state.go('state3');
//start a digest cycle so ui-router will navigate
$rootScope.$apply();
});
});
//set up some dummy controllers and some dummy states
angular.module('app', ['ui.router']).controller('Controller1', function() {
this.message = 'Page 1';
}).controller('Controller2', function() {
this.message = 'Page 2';
}).controller('Controller3', function() {
this.message = 'Page 3';
}).config(function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise("/state1");
$stateProvider.state('state1', {
url: "/state1",
controller: 'Controller1',
controllerAs: 'vm',
template: '<h1>{{vm.message}}</h1>'
}).state('state2', {
url: "/state2",
controller: 'Controller2',
controllerAs: 'vm',
template: '<h2>{{vm.message}}</h2>'
}).state('state3', {
url: "/state3",
controller: 'Controller3',
controllerAs: 'vm',
template: '<h3>{{vm.message}}</h3>'
});
});
h1 {
color: red;
}
h2 {
color: blue;
}
h3 {
color: green;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.0/angular.min.js"></script>
<script src="
https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.18/angular-ui-router.js"></script>
<link rel="stylesheet" type="text/css" href="http://jasmine.github.io/2.0/lib/jasmine.css">
<script src="http://jasmine.github.io/2.0/lib/jasmine.js"></script>
<script src="http://jasmine.github.io/2.0/lib/jasmine-html.js"></script>
<script src="http://jasmine.github.io/2.0/lib/boot.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.0/angular-mocks.js"></script>
<div ng-app="app">
<a ui-sref="state1">State 1</a>
<a ui-sref="state2">State 2</a>
<a ui-sref="state3">State 3</a>
<div ui-view></div>
</div>

If I'm not wrong, I think we missed the point of the initial question, which was
if my view is tags#app.sr.product.upload then how can I test that my
controller is TagsCtrl, my controllerAs value is vm etc??
and
How can I unit test if my state is app.sr.product.upload then
data.tags=[], data.userCommunities=[] etc.
Here's how you can test these :
var $rootScope, $state, $injector, state;
beforeEach(inject(function(_$rootScope_, _$state_){
$rootScope = _$rootScope_;
$state = _$state_;
state = $state.get('app.sr.product.upload');
}));
it('should have the correct data parameters', function () {
expect(state.data.tags).toEqual('');
expect(state.data.userCommunities).toEqual('');
});
it('should render the dashboard views with the right Controllers', function () {
var product = state.views['productView#app.sr.product'];
var tags= state.views['tags#app.sr.product.upload'];
var userCommunity = state.views['UserCommunityPanel#app.sr.product.upload'];
expect(product.templateUrl).toEqual('views/upload/upload.html');
expect(product.controller).toEqual('UploadCtrl');
expect(product.controllerAs).toEqual('ul');
// etc...
});
Also, in newer angular versions, you can just declare your controller like so:
controller: 'UploadCtrl as vm'

It's not something I would normally unit test. UI-Router itself is well covered by tests.
You'd do better with e2e (end-to-end) tests with Protractor. You simulate a click on a link, you expect url to be this, use expect number of elements in a list to be that etc.
But if you really need it:
locate root element of each view (f.e. by adding a specific class and using selectors)
you should be able to access scope
and controller via
angular.element
wrapper methods

Related

Angular 1.x set initial state after page reload

Im using angular 1 for my app + ui router. I want the app to return to the initial state if page reloads. Currently, the app stays on the same page it was before, even after the browser refreshes. How can I make the app to return to login state after page reload? This is my app.js with the config data:
app.config(function($stateProvider, $urlRouterProvider) {
var loginState = {
name: 'login',
url: '/login',
templateUrl: 'templates/login/login.html',
controller: "loginController"
}
var homePageState = {
name: 'homePage',
url: '/homePage',
templateUrl: 'templates/chatRoom/homePage.html',
controller: "homePageController"
}
$stateProvider.state(loginState);
$stateProvider.state(homePageState);
$urlRouterProvider.otherwise('login');
}).run([
"$state",
function($state) {
$state.go('login');
}
]);
app.config(function($stateProvider, $urlRouterProvider) {
//...
}).run([
"$state",
"$rootScope",
function($state, $rootScope) {
$rootScope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState, fromParams) {
if (toState.name == fromState.name) {
$state.go('login');
}
});
}
]);
when refresh, the name of router is same. hope this helps you.
There is a simple javascript event which you can use.
<script>
window.onbeforeunload = function () {
window.location.href = 'http://localhost:61632/index.html#/login';
//Here set the path where you wanted your application to go after reload.
};
</script>
Just add this script tag to your index html file and you are done.

How to set route filters on ui.router states?

In Laravel, setting route filters is quite easy. We use a beforeFilter([]) and specify which functions in the controller should be accessible on authentication, and also have except in exceptional scenarios.
Now, I just started using Angular's ui.router and the concept of states is certainly the way forward, but I am asking a asking a noob question here. How do I set route filters on my states. I definitely don't want to do it on individual routes using resolve.
Here's some code. This is what I use for my profile route. I use resolve to make sure it is only accessible when authenticated. But the problem is, my login and signup routes are still accessible when I am logged in. They shouldn't be. It should just redirect to home.
Now I could add resolves to states I don't want accessible when I am logged in, but is that the right way? What if there are many. That would be repeating code. Is there a 'ui.router` way to do this?
.state('profile', {
url: '/profile',
templateUrl: 'js/app/partials/profile.html',
controller: 'ProfileCtrl',
resolve: {
authenticated: function($q, $location, $auth) {
var deferred = $q.defer();
if (!$auth.isAuthenticated()) {
$location.path('/login');
} else {
deferred.resolve();
}
return deferred.promise;
}
}
})
I think one way is to create a service which does your validation and then in a run block you would call that service on any $stateChangeStart events as K.Toress mentioned.
If you want to specify which states need authenticating, for example, you can use the data option in your state definition config to define whether or not it needs authenticating. So to define a state that needs auth you could try something like...
$stateProvider
.state('foo', {
templateUrl: 'foo.html',
// etc...
data: {
requiresAuth: true
}
});
You can then check this in your $stateChangeStart event which gets passed a toState argument from which you can access the data properties.
var app = angular.module('foo', ['ui.router'])
.factory('RouteValidator', ['$rootScope', '$auth', function($rootScope){
return {
init: init
};
function init(){
$rootScope.$on('$stateChangeStart', _onStateChangeStart);
}
function _onStateChangeStart(event, toState, toParams, fromState, fromParams){
// check the data property (if there is one) defined on your state
// object using the toState param
var toStateRequiresAuth = _requiresAuth(toState),
// if user is not authenticated and the state he is trying to access
// requires auth then redirect to login page
if(!$auth.isAuthenticated() && toStateRequiresAuth){
event.preventDefault();
$state.go('login');
return;
}
}
function _requiresAuth(toState){
if(angular.isUndefined(toState.data) || !toState.data.requiresAuth){
return false;
}
return toState.data.requiresAuth;
}
}])
.run(['RouteValidator', function(RouteValidator){
// inject service here and call init()
// this is so that you keep your run blocks clean
// and because it's easier to test the logic in a service than in a
// run block
RouteValidator.init();
}]);
EDIT
Okay I've made a very basic DEMO on plunker that will hopefully show the concept. I'll post the code here too. Help this helps.
app.js
var app = angular.module('plunker', ['ui.router']);
app.config(['$stateProvider', function($stateProvider){
$stateProvider
.state('public', {
url: "/public",
templateUrl: "public.html"
})
.state('login', {
url: "/login",
templateUrl: "login.html",
controller: function($scope) {
$scope.items = ["A", "List", "Of", "Items"];
}
})
.state('admin', {
url: "/admin",
templateUrl: "admin.html",
data: {
requiresAuth: true
},
controller: function($scope) {
$scope.items = ["A", "List", "Of", "Items"];
}
});
}]);
app.factory('User', [function(){
return {
isAuthenticated: false
};
}]);
app.factory('RouteValidator', ['$rootScope', 'User', '$state', function($rootScope, User, $state){
return {
init: init
};
/////////////////////////////////
function init(){
$rootScope.$on('$stateChangeStart', _onStateChangeStart);
}
function _onStateChangeStart(event, toState, toParams, fromState, fromParams){
var toStateRequiresAuth = _requiresAuth(toState);
if(!User.isAuthenticated && toStateRequiresAuth){
event.preventDefault();
$state.go('public');
alert('You are not authorised to see this view');
return;
}
}
function _requiresAuth(toState){
if(angular.isUndefined(toState.data) || !toState.data.requiresAuth){
return false;
}
return toState.data.requiresAuth;
}
}]);
app.run(['RouteValidator', function(RouteValidator){
RouteValidator.init();
}]);
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
});
index.html
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script>document.write('<base href="' + document.location + '" />');</script>
<link rel="stylesheet" href="style.css" />
<script data-require="angular.js#1.4.x" src="https://code.angularjs.org/1.4.2/angular.js" data-semver="1.4.2"></script>
<script data-require="ui-router#0.2.15" data-semver="0.2.15" src="//rawgit.com/angular-ui/ui-router/0.2.15/release/angular-ui-router.js"></script>
<script src="app.js"></script>
</head>
<body ng-controller="MainCtrl">
<a ui-sref="public">public</a>
<a ui-sref="login">login</a>
<a ui-sref="admin">admin</a>
<div ui-view></div>
</body>
</html>

Angular UI Router Set Scope/Variable

I have an Angular app using the Ui Router for routing purposes. Each time I change the router, I would like to change the header of the page, and it seems like the $stateProvider would be the easiest way to do that. I have something like this for the $stateProvider:
$stateProvider
.state('index', {
url: "/",
views: {
"rightContainer": { templateUrl: "viewA.html" },
},
controller: function ($scope) {
$scope.data.header = "Header 1"
}
})
.state('details', {
url: "/details",
views: {
"rightContainer": { templateUrl: "ViewB.html" },
},
controller: function ($scope) {
$scope.data.header = "Header 2"
}
});
I then want to have the header:
<div data-ng-controller="mainCtrl">
<div class='bg'>{{data.header}}</div>
</div>
You can use data https://github.com/angular-ui/ui-router/wiki#attach-custom-data-to-state-objects
or just an other approach
.run(function ($state,$rootScope$filter,WindowUtils) {
$rootScope.$state = $state;
$rootScope.$on('$stateChangeSuccess', function(event, toState) {
var stateName = toState.name;
//switch
WindowUtils.setTitle(stateName);
});
})
.factory('WindowUtils', function($window) {
return {
setTitle:function(title){
var sep = ' - ';
var current = $window.document.title.split(sep)[0];
$window.document.title = current + sep + title;
}
};
})
The .state object has a data property for exactly what your trying to achieve. Just add data: {header: "Header 1"} to .state like so:
$stateProvider
.state('index', {
url: "/",
views: {
"rightContainer": { templateUrl: "viewA.html" },
},
data: {header: "Header 1"}
})
Edit/Update
To access that data for page titles etc, its best if you use one controller for the index.html <head>, and use $scope.$on to push changes to a $scope.header on route change events. I would recommend having a look at the https://github.com/ngbp/ngbp project boilerplate for a working example.
HTML
https://github.com/ngbp/ngbp/blob/v0.3.2-release/src/index.html
<html ng-app="myApp" ng-controller="AppCtrl">
<head>
<title ng-bind="header"></title>
...
</head>
app.js
https://github.com/ngbp/ngbp/blob/v0.3.2-release/src/app/app.js
.controller( 'AppCtrl', function AppCtrl ( $scope, $location ) {
$scope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams){
if ( angular.isDefined( toState.data.header ) ) {
$scope.header = toState.data.header;
}
});

Angular JS w/ ui.router - Populate multiple state views with unordered gobbed url?

I'm using ui.router with 5 states, each state nested within the previous state. each state has multiple views for different user types. the only state with url's defined are the first: "" and last: "/:params" so that all the states load into the same window.
Now, since each state is in the window, I want to parse the url params to control the app, without needing a set url model, i.e. i want to take an arbitrary number of unordered params and apply them to each state that has a $stateParams $scoped match down the ancestors.
This way we preserve browser history/bookmarking to match ui-state although never needing to actually navigate elsewhere.
How might this be possible?
angular.module('ecoposApp').config(function($stateProvider, $urlRouterProvider, $anchorScrollProvider) {
$stateProvider.
state('ecoApp', {
url:'/',
templateUrl:'views/main.html',
onEnter: function(){
console.log('ecoApp State');
},
onExit: function(){
console.log('goodbye ecoApp state');
}
}).
state('ecoApp.nav',{
views:{
admin:{
template:'<h2 href ng-click="$state.go(\'^\')">Nav Yolo 1</h2>'
},
user:{
template:'<h2 href ng-click="$state.go(\'^\')">Nav Yolo 2</h2>'
}
},
onEnter: function(){
console.log('NAV State');
},
onExit: function(){
console.log('goodbye Navigation state');
}
}).
state('ecoApp.nav.not',{
views:{
admin:{
template:'<h3 href ng-click="$state.go(\'^\')">Notifications Yolo 1</h3>'
},
user:{
template:'<h3 href ng-click="$state.go(\'^\')">Notifications Yolo 2</h3>'
}
},
onEnter: function(){
console.log('notifications state');
},
onExit: function(){
console.log('goodbye Notifications state');
}
}).
state('ecoApp.nav.not.tools',{
views:{
admin:{
template: '<h4 href ng-click="$state.go(\'^\')">Tools Yolo 1</h4>'
},
user:{
template: '<h4 href ng-click="$state.go(\'^\')">Tools Yolo 2</h4>'
}
},
onEnter: function(){
console.log('tools state');
},
onExit: function(){
console.log('goodbye tools state');
}
}).
state('ecoApp.nav.not.tools.settings',{
url:':params',
views: {
admin:{
template:'<a href ng-click="$state.go(\'^\')">Settings Yolo 1</a></div>',
controller: function(){
}
},
user:{
template:'<a href ng-click="$state.go(\'^\')">Settings Yolo 2</a></div>'
}
},
onEnter: function(){
console.log('settings state');
},
onExit: function(){
console.log('goodbye settings state');
}
});
In our last state, we have -
url:'*path?access_level&preferences'
This says the path object on $stateParams can equal anything and to look for access_level and preferences queries such that /settings?access_level=admin&preferences=shop would give this object -
$stateParams: {path:settings,access_level:admin,preferences:shop}
Then we have in our app's .run fn -
$rootScope.$stateParams = $stateParams;
Inside the last state's onEnter fn -
myService.params.data = $stateParams;
if(/^\/settings(\/.*)?$/.test($stateParams.path)){
myService.view = 'admin#ecoApp.nav.not.tools.settings';
}
// the data object acts as a state placeholder so that the same object exists even as we switch states.
In our main controller we inject myService -
$scope.params = myService.params.data;
$scope.myView = myService.params.data.path;
in our main view, we have -
<div ui-view="{{myView}}"></div>
To finish her off, we go back to our app's .run fn to reload the state on $stateChangeSuccess if the toParams and fromParams are different -
$rootScope.$on('$stateChangeSuccess',
function(event, toState, toParams, fromState, fromParams){
if(fromParams !== toParams){
$state.reload();
console.log("%c $state reloaded", "background:#aaa; color:#444")
}
}
);
So given our url of /settings?access_level=admin&preferences=shop we get myView will = admin#ecoApp.nav.not.tools.settings and can change upon new params with switch/ifs.

why $routeChangeSuccess never gets called?

I am doing something similar to below on my app but I am just not able to get the routeChangeSuccess event.
var myapp = angular.module('myapp', ["ui.router", "ngRoute"]);
myapp.controller("home.RootController",
function($rootScope, $scope, $location, $route) {
$scope.menus = [{ 'name': 'Home ', 'link': '#/home'}, {'name': 'services', 'link': '#/services'} ]
$scope.$on('$routeChangeSuccess', function(event, current) {
alert('route changed');
});
}
);
myapp.config(
function($stateProvider, $urlRouterProvider, $routeProvider) {
$urlRouterProvider.otherwise("/home");
$stateProvider
.state('home', {
url: "/home",
//template: '<h1>Home Screen</h1>'
templateUrl: "/Client/Views/Home/Home.htm"
})
.state('services', {
url: "/services",
//template: '<h1>Service screen</h1>'
templateUrl: "/Client/Views/Home/service.htm"
});
});
a very simple html as below also fails
<body ng-controller="home.RootController">
<ul class="nav">
<li ng-repeat="menu in menus" "="">
{{menu.name}}
</li>
</ul>
<div ui-view> No data yet!</div>
</body>
but when i click on the link i see that the views are getting updated but the $routeChangeSucces event is never triggered.
is there something i am missing?
Another question I had was is there an event that I can hook on to to know the view is ready so I can start some additional processing, like document.ready().
plnlr but not fully working...
Regards
Kiran
Please, check this wiki: State Change Events. An extract:
$stateChangeSuccess - fired once the state transition is complete.
$scope.$on('$stateChangeSuccess',
function(event, toState, toParams, fromState, fromParams){ ... })
So instead of the $routeChangeSuccess use the $stateChangeSuccess.
To get more detailed information about all available events, check the wiki Events. Here you can find that the suitable for you, could be event $viewContentLoaded...
stateChange events are now deprecated and removed, use transitions instead.
$transitions.onSuccess({}, function () {
console.log("state changed");
});

Resources