I have 2 roles: one is admin, this another is normal user. Admin can navigate to item-detail page, and normal user can not. Hence, I store user's role in "globals" cookies when they are login. I get their role from cookieStore to check which one can navigate to item-detail page. This function works well. However, I have no idea how to write a test for $cookieStore in checkUserRole function:
angular.module('config', ['ui.router'])
.config(function($stateProvider, $urlRouterProvider)
{
$urlRouterProvider.otherwise('/login');
$stateProvider
.state('login',
{
url: '/login',
templateUrl: 'login-page.html',
controller: 'LoginController'
})
.state('index',
{
url: '/index',
templateUrl: 'bridge.html'
})
.state('item-detail',
{
url: '/index/item-detail/:Name',
templateUrl: 'item-detail.html',
controller: 'myCtrl',
resolve:
{
checkUserRole: function($cookieStore)
{
if($cookieStore.get('globals').currentUser.userRole === 'user')
{
return state.go('index');
}
}
}
});
});
And, here is my test case:
describe('config', function()
{
var $scope, $state, $cookieStore, userRole;
beforeEach(function()
{
module('config', function($provide)
{
$provide.value('$cookieStore', { get: 'globals' });
});
inject(function($injector, $templateCache)
{
$scope = $injector.get('$rootScope');
$state = $injector.get('$state');
$cookieStore = $injector.get('$cookieStore');
$templateCache.put('login-page.html', '');
$templateCache.put('bridge.html', '');
$templateCache.put('item-detail.html', '');
});
});
it('home page', function()
{
$scope.$apply();
expect($state.current.name).toBe('login');
expect($state.current.templateUrl).toBe('login-page.html');
expect($state.current.controller).toBe('LoginController');
});
it('login page', function()
{
$scope.$apply(function()
{
$state.go('login');
});
expect($state.current.name).toBe('login');
expect($state.current.templateUrl).toBe('login-page.html');
expect($state.current.controller).toBe('LoginController');
});
it('items page', function()
{
$scope.$apply(function()
{
$state.go('index');
});
expect($state.current.name).toBe('index');
expect($state.current.templateUrl).toBe('bridge.html');
});
it('item-detail page', function()
{
spyOn($cookieStore, 'get').and.callFake(function()
{
return 'user';
});
expect($cookieStore.get('globals')).toBe('user');
$scope.$apply(function()
{
$state.go('item-detail');
});
expect($state.current.name).toBe('item-detail');
expect($state.current.templateUrl).toBe('item-detail.html');
expect($state.current.controller).toBe('myCtrl');
expect($state.href('item-detail', { Name: 'lumia-950'})).toEqual('#/index/item-detail/lumia-950');
});
});
My question is: How can I write a test for $cookieStore.get('globals').currentUser.userRole? or how can I mock it to test what if user's role is user?.
I don't know what version of angular you're using but $cookieStore is now deprecated, prefer using $cookies instead (see doc).
There are at least 3 ways to proceed :
Using Jasmine with Angular $cookie (from v1.4.x) :
describe('config module', function(){
var $cookies;
beforeEach(function(){
angular.mock.module('config');
angular.mock.inject(function(_$cookies_){
$cookies = _$cookies_;
});
});
it('should have a "globals" cookie with "user" value', function(){
var globals = $cookies.getObject('globals');
expect(globals.currentUser.userRole).toBe('user');
});
});
Using Jasmine with pure JavaScript :
describe('config module', function(){
it('should have a "globals" cookie with "user" value', function(){
var globals = document.cookie.replace(/(?:(?:^|.*;\s*)globals\s*\=\s*([^;]*).*$)|^.*$/, "$1");
globals = JSON.parse(globals);
expect(globals.currentUser.userRole).toBe('user');
});
});
Using Jasmine with Protractor (e2e test) :
describe('config module', function(){
it('should have a "globals" cookie with "user" value', function(){
var globals = JSON.parse(browser.manage().getCookie('globals'));
expect(globals.currentUser.userRole).toBe('user');
});
});
Here Protractor gives us browser global variable to handle browser related behavior.
Note that JSON.parse() is used to unserialized the cookie string value and parse it to an usable JavaScript object.
example :
var obj = JSON.parse('{ "foo" : "bar" }');
console.log(obj.foo); //print 'bar' in the console
IMPORTANT:
In your application, use only 1.4.7 version of Angular libraries with the Angular 1.4.7 Core.
Let me know if it helps.
Related
I have a page where everything works fine. However, my test for this page's controller FeeRuleCtrl, after it tests the code of said controller, goes on and starts testing the controller of a different state. Here's my app.js:
$stateProvider
.state('root', {
url: "/",
templateUrl: "<%= Rails.application.routes.url_helpers.client_side_path('admin/fee_suites/root') %>",
controller: 'RootCtrl',
resolve: {
feeSuites: function(FeeSuiteCrud, FeeSuite){
console.log('here');
var feeCrud = new FeeSuiteCrud(FeeSuite);
var promise = feeCrud.query();
return promise.then(function(response){
return response;
});
}
}
})
.state('fee-rule', {
abstract: true,
controller: 'FeeRuleCtrl',
template: "<ui-view/>",
resolve: {
feeTypes: function(FeeSuiteCrud, FeeType){
var feeCrud = new FeeSuiteCrud(FeeType)
var promise = feeCrud.query();
return promise.then(function(response){
return response;
})
},
feeSuites: function(FeeSuiteCrud, FeeSuite){
var feeCrud = new FeeSuiteCrud(FeeSuite);
var promise = feeCrud.query();
return promise.then(function(response){
return response;
});
}
}
})
.state('fee-rule.new', {
url: '/new',
controller: 'NewCtrl',
templateUrl: "<%= Rails.application.routes.url_helpers.client_side_path('admin/fee_suites/feeRule.html') %>",
data: { title: 'Add a New Fee Rule' }
})
.state('fee-rule.edit', {
url: "/edit/:id",
controller: 'EditCtrl',
templateUrl: "<%= Rails.application.routes.url_helpers.client_side_path('admin/fee_suites/feeRule.html') %>",
data: { title: 'Edit Fee Rule' },
resolve: {
feeRule: function(FeeSuiteCrud, FeeRule, $stateParams){
var feeCrud = new FeeSuiteCrud(FeeRule);
var promise = feeCrud.get($stateParams.id)
return promise.then(function(response){
return response;
});
}
}
});
I have an abstract state, fee-rule, because both the new and edit states share most of the same functionality.
When I go to the page's address, <host>/admin/fee_suites/new, I inspect the network tab and there are 4 server calls made:
api/v3/fee_types
api/v3/fee_suites
api/v3/fee_suites/8?association=fee_rules
api/v3/fee_types/9?association=fee_parameters
The first 2 are resolves in the fee-rule state. I take care of this like so in the test:
beforeEach(function(){
module(function($provide){
$provide.factory('feeSuites', function(FeeSuite){
feeSuite = new FeeSuite({
id: 8,
site_id: 9,
active: true
});
return [feeSuite];
});
$provide.factory('feeTypes', function(FeeType){
feeType = new FeeType({
id: 9,
name: 'Carrier Quotes',
value: 'carrier_quotes'
});
return [feeType];
});
});
inject(function($injector){
$rootScope = $injector.get('$rootScope');
$controller = $injector.get('$controller');
$httpBackend = $injector.get('$httpBackend');
scope = $rootScope.$new();
$controller("FeeRuleCtrl", {
'$scope': scope
});
});
});
The last 2 server calls are made inside FeeRuleCtrl. I test them like so:
beforeEach(function(){
var JSONResponse = {"master":[{"id":29,"fee_suite_id":8,"fee_parameter_id":1,"name":"American Express Fee","multiplier":0.045,"addend":0.0,"order":1,"order_readonly":true,"created_at":"2016-10-17T14:20:08.000-05:00","updated_at":"2016-10-17T14:20:08.000-05:00"},{"id":30,"fee_suite_id":8,"fee_parameter_id":2,"name":"Discover Fee","multiplier":0.045,"addend":0.0,"order":1,"order_readonly":true,"created_at":"2016-10-17T14:20:08.000-05:00","updated_at":"2016-10-17T14:20:08.000-05:00"},{"id":31,"fee_suite_id":8,"fee_parameter_id":3,"name":"MasterCard Fee","multiplier":0.045,"addend":0.0,"order":1,"order_readonly":true,"created_at":"2016-10-17T14:20:08.000-05:00","updated_at":"2016-10-17T14:20:08.000-05:00"},{"id":32,"fee_suite_id":8,"fee_parameter_id":4,"name":"Visa Fee","multiplier":0.045,"addend":0.0,"order":1,"order_readonly":true,"created_at":"2016-10-17T14:20:08.000-05:00","updated_at":"2016-10-17T14:20:08.000-05:00"}]};
$httpBackend.expectGET('/api/v3/fee_suites/8?association=fee_rules').respond(JSONResponse);
JSONResponse = {"master":[{"id":25,"fee_type_id":9,"name":"UPS Published Quote","value":"ups_published_quote","parameter_type":"currency","created_at":"2016-10-17T14:20:08.000-05:00","updated_at":"2016-10-17T14:20:08.000-05:00"},{"id":26,"fee_type_id":9,"name":"FedEx Published Quote","value":"fedex_published_quote","parameter_type":"currency","created_at":"2016-10-17T14:20:08.000-05:00","updated_at":"2016-10-17T14:20:08.000-05:00"},{"id":27,"fee_type_id":9,"name":"UPS Negotiated Quote","value":"ups_negotiated_quote","parameter_type":"currency","created_at":"2016-10-17T14:20:08.000-05:00","updated_at":"2016-10-17T14:20:08.000-05:00"},{"id":28,"fee_type_id":9,"name":"FedEx Negotiated Quote","value":"fedex_negotiated_quote","parameter_type":"currency","created_at":"2016-10-17T14:20:08.000-05:00","updated_at":"2016-10-17T14:20:08.000-05:00"}]};
$httpBackend.expectGET('/api/v3/fee_types/9?association=fee_parameters').respond(JSONResponse);
$httpBackend.flush();
});
it('should set currentFeeRuleNum', function(){
expect(scope.FeeSuite.currentFeeRuleNum).toEqual(4);
});
When I run my test I get the following error:
Error: Unexpected request: GET /api/v3/fee_suites/
I know it is coming from root state's resolve function feeSuites because the test also prints to the console log the word 'here'.
I cannot figure out why it seems like the test doesn't stop and starts testing the RootCtrl in the root state. Could it have anything to do with the fact that state fee-rule is abstract? Also NewCtrl is defined but it is empty.
After some more googling with different keywords, turns out in my test I need to mock the $state variable inside FeeRuleCtrl. That fixed the problem.
I followed this post (http://gonehybrid.com/how-to-write-automated-tests-for-your-ionic-app-part-2/) to create a simple unit test using Karma & Jasmine for a Ionic controller, but i keep getting undefined errors while the stated objects have been defined. I'm i missing something obvious? By the way, i'm able to run referenced tests from the blog above successfully which makes me think i'm missing something in mine.
Errora are as follows:
TypeError: undefined is not an object (evaluating 'authMock.login') in /Users/projects/app/tests/unit-tests/login.controller.tests.js (line 65)
TypeError: undefined is not an object (evaluating 'deferredLogin.resolve') in /Users/projects/app/tests/unit-tests/login.controller.tests.js (line 71)
TypeError: undefined is not an object (evaluating 'deferredLogin.reject') in /Users/projects/app/tests/unit-tests/login.controller.tests.js (line 79)
Here's the controller:
angular.module('app').controller('LoginCtrl', function($scope, $state, $ionicPopup, $auth) {
$scope.loginData = {};
$scope.user = {
email: '',
password: ''
};
$scope.doLogin = function(data) {
$auth.login(data).then(function(authenticated) {
$state.go('app.tabs.customer', {}, {reload: true});
}, function(err) {
var alertPopup = $ionicPopup.alert({
title: 'Login failed!',
template: 'Please check your credentials!'
});
});
};
});
Here's the test:
describe('LoginCtrl', function() {
var controller,
deferredLogin,
$scope,
authMock,
stateMock,
ionicPopupMock;
// load the module for our app
beforeEach(angular.mock.module('app'));
// disable template caching
beforeEach(angular.mock.module(function($provide, $urlRouterProvider) {
$provide.value('$ionicTemplateCache', function(){} );
$urlRouterProvider.deferIntercept();
}));
// instantiate the controller and mocks for every test
beforeEach(angular.mock.inject(function($controller, $q, $rootScope) {
deferredLogin = $q.defer();
$scope = $rootScope.$new();
// mock dinnerService
authMock = {
login: jasmine.createSpy('login spy')
.and.returnValue(deferredLogin.promise)
};
// mock $state
stateMock = jasmine.createSpyObj('$state spy', ['go']);
// mock $ionicPopup
ionicPopupMock = jasmine.createSpyObj('$ionicPopup spy', ['alert']);
// instantiate LoginController
controller = $controller('LoginCtrl', {
'$scope': $scope,
'$state': stateMock,
'$ionicPopup': ionicPopupMock,
'$auth': authMock
});
}));
describe('#doLogin', function() {
// call doLogin on the controller for every test
beforeEach(inject(function(_$rootScope_) {
$rootScope = _$rootScope_;
var user = {
email: 'test#yahoo.com',
password: 'test'
};
$scope.doLogin(user);
}));
it('should call login on $auth Service', function() {
expect(authMock.login).toHaveBeenCalledWith(user);
});
describe('when the login is executed,', function() {
it('if successful, should change state to app.tabs.customer', function() {
deferredLogin.resolve();
$rootScope.$digest();
expect(stateMock.go).toHaveBeenCalledWith('app.tabs.customer');
});
it('if unsuccessful, should show a popup', function() {
deferredLogin.reject();
$rootScope.$digest();
expect(ionicPopupMock.alert).toHaveBeenCalled();
});
});
})
});
Here's my Karma config:
files: [
'../www/lib/ionic/js/ionic.bundle.js',
'../www/lib/angular-mocks/angular-mocks.js',
'../www/js/*.js',
'../www/js/**/*.js',
'unit-tests/**/*.js'
],
I think that your controller for tests is undefined. Try to replace first it function with this and check if is it defined.
it('controller to be defained', function() {
expect($controller).toBeDefined();
});
If it isn't, try to call controller with:
$controller = _$controller_;
I'm trying to mock the resolve functions inside the $state of my ui-router file, but I can't seem to get it to work. Here's my router code:
$stateProvider
.state('state.of.page', {
url: '/url-to-page',
template: require('./page-template.html'),
controller: 'PageCtrl',
controllerAs: 'page',
resolve: {
/**
* Cloning of page object in case user leaves page
* without saving
*/
pageObjClone: ['pageObj', function (pageObj) {
return angular.copy(pageObj);
}],
pageTemplate: ['template', function (template) {
return template;
}]
}
Here is my Jasmine code. I'm currently getting the error 'fn' is not a function when I run the test.
'use strict';
describe('my jasmine $state test', function() {
var $state;
var $injector;
var stateName = 'state.of.page';
var stateObj;
beforeEach(function() {
angular.mock.module('MyApp');
inject(function(_$rootScope_, _$state_, _$injector_) {
$state = _$state_;
$injector = _$injector_;
stateObj = {
customerObjClone: ['customerObj', function (customerObj) {}],
template: ['template', function (template) {}]
};
});
});
it('should resolve data', function() {
var state = $state.get(stateName);
expect($injector.invoke($state.resolve)).toBe('stateObj');
});
});
Thanks for any help.
...
var pageObj, template;
beforeEach(function() {
angular.mock.module('MyApp');
inject(function(..., _pageObj_, _template_) {
...
pageObj = _pageObj_;
template = _template_;
});
});
it('should resolve data', function() {
var state = $state.get(stateName);
expect($injector.invoke(state.resolve.pageObjClone)).toEqual(pageObj);
expect($injector.invoke(state.resolve.pageTemplate)).toBe(template);
});
Depending on how complex resolvers and their dependencies are, the dependencies may be mocked and injected into resolver, e.g.
expect($injector.invoke(state.resolve.pageTemplate, null, { template: mockedTemplate }))
.toBe(mockedTemplate);
I am working on an app where I need to resolve promises in the router (ngRoute). The problem is that I am not sure how to write the unit tests for this, I am using karma with mocha and chai.
Here is the part of the code I'd like to test:
function config ($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'views/orders.html',
controller: 'OrderController',
controllerAs: 'vmr',
resolve: OrderController.resolve,
data: {...}
});
}
function OrderController (OrderService, newOrders) {
this.newOrders = newOrders;
}
OrderController.resolve = {
newOrders: function (OrderService) {
return OrderService.getOrders();
}
};
This is how I started to write my unit tests when I didn't have the resolve part yet:
describe('OrderController', function() {
'use strict';
var controller,
service,
httpBackend;
beforeEach(module('myApp.orders'));
beforeEach(inject(function($controller, _OrderService_, $httpBackend) {
service = _OrderService_;
httpBackend = $httpBackend;
// Create the controller
controller = $controller('OrderController', {});
}));
beforeEach(function() {
httpBackend.when('GET', 'url/to/get/orders')
.respond(200, {[...]});
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
httpBackend.verifyNoOutstandingRequest();
});
it('should get the list of new orders', function() {
httpBackend.flush();
expect(controller.neworders).not.to.undefined;
expect(controller.neworders.length).to.equal(3);
});
});
At this point is where I am getting the error:
Unknown provider: newOrdersProvider <- newOrders
I understand why I get this error, but I don't know how to solve it. Basically I don't know how to test the promise that resolves in the route.
Thanks in advance for your help!
After a lot of searching and reading the AngularJS Testing Cookbook I find out how to inject the result of the promise in the controller.
The main code doesn't change, so I will post here only the update code for the unit tests:
describe('OrderController', function() {
'use strict';
var controller,
service,
httpBackend;
// here is where I will inject a new value
beforeEach(function() {
module('myApp.orders', function($provide) {
$provide.value('resolver', {
newOrders: function(service) {
return service.getOrders();
}
});
});
});
beforeEach(inject(function($controller, _OrderService_, $httpBackend, resolver) {
service = _OrderService_;
httpBackend = $httpBackend;
// Create the controller
controller = $controller('OrderController', {
// add them to the controller
newOrders: resolver.newOrders(service)
});
}));
beforeEach(function() {
httpBackend.when('GET', 'url/to/get/orders')
.respond(200, {[...]});
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
httpBackend.verifyNoOutstandingRequest();
});
it('should get the list of new orders', function() {
httpBackend.flush();
expect(controller.neworders).not.to.undefined;
expect(controller.neworders.length).to.equal(3);
});
});
If someone has a better/different solution I'd like to hear it as well!
I have a controller & view which computes and displays a specific element. When I click a button on the view, I want to open another which shows more information about the item.
What is the best way to pass the element to another controller for display?
Many thanks
You could build a service which get data from one controller and passing it to the other, like so:
.factory('shareData', function(){
var finalData;
return {
sendData: function(data) {
finalData = data;
},
getData: function() {
return finalData;
}
}
});
Another option you have is to create a shared varraible in your main controller('app' controller in ionic)
.controller('app', function($scope){
$scope.data;
})
.controller('app.controller1', function($scope){
$scope.data = "Hello World";
})
.controller('app.controller2', function($scope){
console.log($scope.data); //Hello World
});
Yet another option besides those included by Matan Gubkin is including the array in the $rootScope: this way you will be able to access it from everywhere.
#Matan Gubkin idea is close, but you should use the service to get the specific item that you desire.
.factory('shareData', function(){
var finalData = ["Hello World",'more data'];
return {
sendData: function(data) {
finalData = data;
},
getData: function() {
return finalData;
},
getItem:function(_id) {
return finalData[_id];
}
}
});
then in setting up your controllers
.controller('app.controller', function($scope){
})
.controller('app.controller-list', function($scope,shareData){
$scope.data = shareData.getData();
})
.controller('app.controller-detail', function($scope,$stateParams,shareData){
// use stateParams to get index
console.log(shareData.getItem($stateParams.index)); //
});
setting up your routes
.config(function($stateProvider, $urlRouterProvider) {
$stateProvider
.state('app', {
url: "/app",
abstract: true,
controller: 'app.controller'
})
.state('app.controller-list', {
url: "/list",
})
.state('app.controller-detail', {
url: "/detail/:index", // <-- pass in index as stateParam
});
// if none of the above states are matched, use this as the fallback
$urlRouterProvider.otherwise('/app/list');
});