AngularJS: unit testing ui-router state changes - angularjs

I am fairly new to unit testing in angular so bear with me please!
I have a $stateProvidor setup for my app and would like to test that the routing part does work correctly.
Say I have this sort of config:
angular.module("app.routing.config", []).config(function($stateProvider, $urlRouterProvider) {
$stateProvider.state("home", {
url: "/",
templateUrl: "app/modules/home/page/home_page.html",
controller: "HomePageController",
resolve: {
setPageTitle: function($rootScope) {
return $rootScope.pageTitle = "Home";
}
}
}).state("somethingelse", {
url: "/",
templateUrl: "app/modules/home/page/somethingelse.html",
controller: "SomeThingElseController",
resolve: {
setPageTitle: function($rootScope) {
return $rootScope.pageTitle = "Some Thing Else";
}
}
});
return $urlRouterProvider.otherwise('/');
});
I came across this blog post on how to set up unit testing for a ui-router config, so I Have tried to adopt the same approach, here is my test I am trying out:
'use strict';
describe('UI-Router State Change Tests', function() {
var $location, $rootScope, $scope, $state, $templateCache;
beforeEach(module('app'));
beforeEach(inject(function(_$rootScope_, _$state_, _$templateCache_, _$location_) {
$rootScope = _$rootScope_;
$state = _$state_;
$templateCache = _$templateCache_;
$location = _$location_;
}));
describe('State Change: home', function() {
beforeEach(function() {
$templateCache.put(null, 'app/modules/home/page/home_page.html');
});
it('should go to the home state', function() {
$location.url('home');
$rootScope.$digest();
expect($state.href('home')).toEqual('#/');
expect($rootScope.pageTitle).toEqual('Home');
});
});
});
When running the test I am getting this error in the output:
Error: Unexpected request: GET app/modules/home/page/home_page.html
Clearly I am doing something wrong here, so any help or pointers would be much appreciated.
I did come across $httpBackend, is this something I should also be using here, so telling my test to expect a request to the html page my state change test is making?

This is almost certainly down to a partial html view (home_page.html) being loaded asynchronously during app / test runtime.
In order to handle this, you can preprocess your html partials into Javascript strings, which can then be loaded synchronously via your tests.
Have a look at karma-ng-html2js-preprocessor which should solve your problem.

Related

Stubbing factory being used in resolve

I'm trying to unit test my states in an controller. What I want to do is stub out my items factory, since I have separate unit tests that cover that functionality. I'm having a hard time getting the $injector to actually inject the factory, but it seems like I'm letting the $provider know that I want to use my fake items object when it instantiates the controller. As a disclaimer I'm brand new to angular and would love some advice if my code looks bad.
Currently when I run the test I get the message:
Error: Unexpected request: GET /home.html
No more request expected
at $httpBackend (node_modules/angular-mocks/angular-mocks.js:1418:9)
at n (node_modules/angular/angular.min.js:99:53)
at node_modules/angular/angular.min.js:96:262
at node_modules/angular/angular.min.js:131:20
at m.$eval (node_modules/angular/angular.min.js:145:347)
at m.$digest (node_modules/angular/angular.min.js:142:420)
at Object.<anonymous> (spec/states/homeSpec.js:29:16)
It appears that my mocked items factory isn't being injected into the test. When I place a console.log line in the method I want to stub in the items factory I see that line being invoked.
The code I'm looking to test is as follows:
angular.module('todo', ['ui.router'])
// this is the factory i want to stub out...
.factory('items', ['$http', function($http){
var itemsFactory = {};
itemsFactory.getAll = function() {
// ...specifically this method
};
return itemsFactory;
}])
.controller('TodoCtrl', ['$scope', 'items', function($scope, items) {
// Do things
}])
.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider){
$stateProvider
.state('home', {
url: '/home',
templateUrl: '/home.html',
controller: 'TodoCtrl',
resolve: {
items: ['items', function(items){
// this is the invocation that i want to use my stubbed method
return items.getAll();
}]
}
});
$urlRouterProvider.otherwise('home');
}]);
My test looks like this:
describe('home state', function() {
var $rootScope, $state, $injector, state = 'home';
var getAllStub = sinon.stub();
var items = {
getAll: getAllStub
};
beforeEach(function() {
module('todo', function($provide) {
$provide.value('items', items);
});
inject(function(_$rootScope_, _$state_, _$injector_) {
$rootScope = _$rootScope_;
$state = _$state_;
$injector = _$injector_;
});
});
it('should resolve items', function() {
getAllStub.returns('getAll');
$state.go(state);
$rootScope.$digest();
expect($state.current.name).toBe(state);
expect($injector.invoke($state.current.resolve.items)).toBe('findAll');
});
});
Thanks in advance for your help!
Allowing real router in unit tests is a bad idea because it breaks the isolation and adds more moving parts. I personally consider $stateProvider, etc. stubs a better testing strategy.
The order matters in config blocks, service providers should be mocked before they will be injected in other modules. If the original modules have config blocks that override mocked service providers, the modules should be stubbed:
beforeAll(function () {
angular.module('ui.router', []);
});
beforeEach(function () {
var $stateProviderMock = {
state: sinon.stub().returnsThis()
};
module(function($provide) {
$provide.constant('$stateProvider', $stateProviderMock);
});
module('todo');
});
You just need to make sure that $stateProvider.state is called with expected configuration objects an arguments:
it('should define home state', function () {
expect($stateProviderMock.state.callCount).to.equal(1);
let [homeStateName, homeStateObj] = $stateProviderMock.state.getCall(0).args;
expect(homeStateName).to.equal('home');
expect(homeState).to.be.an('object');
expect(homeState.resolve).to.be.an('object');
expect(homeState.resolve.items).to.be.an('array');
let resolvedItems = $injector.invoke(homeState.resolve.items);
expect(items.getAll).to.have.been.calledOnce;
expect(resolvedItems).to.equal('getAll');
...
});

Unit Testing UI Router With Resolve

I am trying to write a unit test for my ui-router with uses resolve.
Router.js
define(['module', 'require'], function(module, require) {
'use strict';
var Router = function ($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/shopping');
$stateProvider
.state('shopping', {
url: '/shopping',
templateUrl: 'app/shopping.html',
resolve:{
userFactory : 'UserFactory',
checkAccess:function(userFactory){
return userFactory.checkUser();
}
}
})
.state('products', {
url: '/products',
templateUrl: 'app/products.html',
resolve:{
userFactory : 'UserFactory',
checkAccess:function(userFactory){
return userFactory.checkUser();
}
}
})
.state('notAuth', {
url: '/notAuth',
templateUrl: 'app/unauthorised.html'
});
};
module.exports = ['$stateProvider', '$urlRouterProvider', Router];
});
Within userFactory.checkUser(); i essentially check the users' rights, and redirect either to the templateUrl, or perform a:
$state.go('notAuth');
My currect .spec.js:
define(['require', 'angular-mocks', 'angular-ui-router', 'app/router'], function (require) {
'use strict';
describe('myApp/myState', function() {
var $rootScope, $state, $injector, myServiceMock, $httpBackend, state = 'shopping';
var mockResponse = {
"access": true
}
var angular = require('angular');
var myRouter = require('app/router');
beforeEach(module('ui.router'));
beforeEach(function() {
module(myRouter, function($provide) {
$provide.value('userFactory', myServiceMock = {});
});
inject(function(_$rootScope_, _$state_, _$injector_, $templateCache, _$httpBackend_) {
$rootScope = _$rootScope_.$new();
$state = _$state_;
$injector = _$injector_;
$httpBackend = _$httpBackend_;
$rootScope.$digest();
})
});
it('should transition to shopping', inject(function($state,$rootScope){
$state.transitionTo('shopping');
$rootScope.$apply();
expect($state.current.name).toBe('shopping');
}));
it('should resolve data', function() {
myServiceMock.checkUser = jasmine.createSpy('checkUser').and.returnValue(mockResponse);
// earlier than jasmine 2.0, replace "and.returnValue" with "andReturn"
$state.go(state);
$rootScope.$digest();
expect($state.current.name).toBe(state);
});
});
});
With the above test, i get the following errors:
Information: myApp/myState
Information: should transition to shopping Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!
Information: Watchers fired in the last 5 iterations: []
Information: http://errors.angularjs.org/1.4.0/$rootScope/infdig?p0=10&p1=%5B%5D
Information: should resolve data Expected '' to be 'shopping'.
It looks like there are a few things happening here. First, you're digesting too many times. That's the "10 $digest() iterations reached" message. To fix that one, you should remove the $rootScope.$digest() from the end of your inject function.
I think the actual failing test part is because you're not resolving your checkAccess resolve that you've configured in your route. Try adding $provide.value('checkAccess', function() {return true;}); to your $provide.value setup. That should allow checkAccess to resolve in your route, which will then allow the route to complete the state transition.
I found my answer to a similar question here: Angular Jasmine UI router inject resolve value into test

Testing a service that depends on $state

I've been trying to write a unit test for my Progress Service, which manages the different states of a flow (jumps to next incomplete step, marks step as complete, etc.)
I'm trying to configure $stateProvider to create a set of states and then test the service against it, but I can't get the state to change. Of course I would like to test this in an isolated way, so not depending on existing states in my application.
Here's the simple unit test:
ddescribe('Progress Steps Service', function() {
var sut, $rootScope, $state;
// holds ui.router and ProgressStepsService deps
beforeEach(module('Core'));
beforeEach(inject(
function(_$rootScope_, _$state_) {
$rootScope = _$rootScope_;
$state = _$state_;
}
));
beforeEach(inject(function(ProgressStepsService) {
// var steps = [...];
// sut = ProgressStepsService;
// sut.resolve(steps);
angular.module('TestModule', []).config(function($stateProvider) {
$stateProvider
.state('root', {url: '/root', abstract: true})
.state('root.step1', {url: '/step1'})
.state('root.step2', {url: '/step2'})
.state('root.step3', {url: '/step3'});
});
$state.go('root.step1');
$rootScope.digest();
}));
it('should init from the first step', function() {
expect($state.$current.name).toBe('root.step1');
});
});
The error I'm getting is
Error: Could not resolve 'root.step1' from state ''
I take '' is the default state, and for some reason the configured states are not navigable. I'm probably missing something very basic.
How should I configure the $stateProvider to test against arbitrary states?
Ok, so here it is what I think was happening:
I was loading $state before configuring it via $stateProvider, and in addition, I was not injecting the TestModule.
I've updated the code to look like this:
ddescribe('Progress Steps Service', function() {
var sut, $state, $rootScope;
beforeEach(function() {
// var steps = [...];
// sut = ProgressStepsService;
// sut.resolve(steps);
angular.module('TestModule', []).config(function($stateProvider) {
$stateProvider
.state('root', {url: '/root', abstract: true})
.state('root.step1', {url: '/step1'})
.state('root.step2', {url: '/step2'})
.state('root.step3', {url: '/step3'});
});
module('Core', 'TestModule');
inject(function(){});
});
beforeEach(inject(
function(_$state_, _$rootScope_) {
$state = _$state_;
$rootScope = _$rootScope_;
}
));
it('should transition to the first incomplete state', function() {
$state.go('root.step1');
$rootScope.$digest();
expect($state.$current.name).toBe('root.step1');
});
});
which does the trick.

lazy loading angularjs controllers with ui-router resolve

I'm trying to get to work angular.js, ui-router, and require.js and feel quite confused. I tried to follow this tutorial http://ify.io/lazy-loading-in-angularjs/. First, let me show you my code:
app.js =>
var app = angular.module('myApp', []);
app.config(function ($stateProvider, $controllerProvider, $compileProvider, $filterProvider, $provide) {
$stateProvider.state('home',
{
templateUrl: 'tmpl/home-template.html',
url: '/',
controller: 'registration'
resolve: {
deps: function ($q, $rootScope) {
var deferred = $q.defer(),
dependencies = ["registration"];
require(dependencies, function () {
$rootScope.$apply(function () {
deferred.resolve();
});
})
return deferred.$promise;
}
}
}
);
app.lazy = {
controller: $controllerProvider.register,
directive: $compileProvider.directive,
filter: $filterProvider.register,
factory: $provide.factory,
service: $provide.service
};
});
Now in my registration.js I have following code:
define(["app"], function (app) {
app.lazy.controller("registration" , ["$scope", function ($scope) {
// The code here never runs
$scope.message = "hello world!";
}]);
});
everything works well, even the code in registration.js is run. but the problem is code inside controller function is never run and I get the error
Error: [ng:areq] http://errors.angularjs.org/1.2.23/ng/areq?p0=registration&p1=not a function, got undefined
Which seems my code does not register controller function successfully. Any Ideas?
P.s. In ui-router docs it is said "If any of these dependencies are promises, they will be resolved and converted to a value before the controller is instantiated and the $routeChangeSuccess event is fired." But if I put the deferred.resolve(); from mentioned code inside a timeOut and run it after say 5 seconds, my controller code is run and my view is rendered before resolve, Strange.
Seems like I ran into the exact same problem, following the exact same tutorial that you did, but using ui-router. The solution for me was to:
Make sure the app.controllerProvider was available to lazy controller script. It looked like you did this using app.lazy {...}, which a really nice touch BTW :)
Make sure the lazy ctrl script uses define() and not require() I couldn't tell from your code if you had done this.
Here is my ui-router setup with the public app.controllerProvider method:
app.config(function ($stateProvider, $controllerProvider, $filterProvider, $provide, $urlRouterProvider) {
app.lazy = {
controller: $controllerProvider.register,
directive: $compileProvider.directive,
filter: $filterProvider.register,
factory: $provide.factory,
service: $provide.service
};
$urlRouterProvider.otherwise('/');
$stateProvider
.state('app', {
url:'/',
})
.state('app.view-a', {
views: {
'page#': {
templateUrl: 'view-a.tmpl.html',
controller: 'ViewACtrl',
resolve: {
deps: function ($q, $rootScope) {
var deferred = $q.defer();
var dependencies = [
'view-a.ctrl',
];
require(dependencies, function() {
$rootScope.$apply(function() {
deferred.resolve();
});
});
return deferred.promise;
}
}
}
}
});
});
Then in my lazy loaded controller I noticed that I had to use require(['app']), like this:
define(['app'], function (app) {
return app.lazy.controller('ViewACtrl', function($scope){
$scope.somethingcool = 'Cool!';
});
});
Source on GitHub: https://github.com/F1LT3R/angular-lazy-load
Demo on Plunker: http://plnkr.co/edit/XU7MIXGAnU3kd6CITWE7
Changing you're state's url to '' should do the trick. '' is root example.com/, '/' is example.com/#/.
I came to give my 2 cents. I saw you already resolved it, I just want to add a comment if someone else have a similar problem.
I was having a very similar issue, but I had part of my code waiting for the DOM to load, so I just called it directly (not using the "$(document).ready") and it worked.
$(document).ready(function() { /*function was being called here*/ });
And that solved my issue. Probably a different situation tho but I was having the same error.

Unit testing controller which uses $state.transitionTo

within a controller i have a function which uses $state.transitionTo to "redirect" to another state.
now i am stuck in testing this function, i get always the error Error: No such state 'state-two'. how can i test this? it its totally clear to me that the controller does not know anything about the other states, but how can i mock this state?
some code:
angular.module( 'mymodule.state-one', [
'ui.state'
])
.config(function config($stateProvider) {
$stateProvider.state('state-one', {
url: '/state-one',
views: {
'main': {
controller: 'MyCtrl',
templateUrl: 'mytemplate.tpl.html'
}
}
});
})
.controller('MyCtrl',
function ($scope, $state) {
$scope.testVar = false;
$scope.myFunc = function () {
$scope.testVar = true;
$state.transitionTo('state-two');
};
}
);
describe('- mymodule.state-one', function () {
var MyCtrl, scope
beforeEach(module('mymodule.state-one'));
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
MyCtrl = $controller('MyCtrl', {
$scope: scope
});
}));
describe('- myFunc function', function () {
it('- should be a function', function () {
expect(typeof scope.myFunc).toBe('function');
});
it('- should test scope.testVar to true', function () {
scope.myFunc();
expect(scope.testVar).toBe(true);
expect(scope.testVar).not.toBe(false);
});
});
});
Disclaimer: I haven't done this myself, so I totally don't know if it will work and is what your are after.
From the top of my head, two solutions come to my mind.
1.) In your tests pre configure the $stateProvider to return a mocked state for the state-two That's also what the ui-router project itself does to test state transitions.
See: https://github.com/angular-ui/ui-router/blob/04d02d087b31091868c7fd64a33e3dfc1422d485/test/stateSpec.js#L29-L42
2.) catch and parse the exception and interpret it as fulfilled test if tries to get to state-two
The second approach seems very hackish, so I would vote for the first.
However, chances are that I totally got you wrong and should probably get some rest.
Solution code:
beforeEach(module(function ($stateProvider) {
$stateProvider.state('state-two', { url: '/' });
}));
I recently asked this question as a github issue and it was answered very helpfully.
https://github.com/angular-ui/ui-router/issues/537
You should do a $rootScope.$apply() and then be able to test. Note that by default if you use templateUrl you will get an "unexpected GET request" for the view, but you can resolve this by including your templates into your test.
'use strict';
describe('Controller: CourseCtrl', function () {
// load the controller's module
beforeEach(module('myApp'));
// load controller widgets/views/partials
var views = [
'views/course.html',
'views/main.html'
];
views.forEach(function(view) {
beforeEach(module(view));
});
var CourseCtrl,
scope;
// Initialize the controller and a mock scope
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
CourseCtrl = $controller('CourseCtrl', {
$scope: scope
});
}));
it('should should transition to main.course', inject(function ($state, $rootScope) {
$state.transitionTo('main.course');
$rootScope.$apply();
expect($state.current.name).toBe('main.course');
}));
});
Also if you want to expect on that the transition was made like so
expect(state.current.name).toEqual('state-two')
then you need to scope.$apply before the expect() for it to work

Resources