I've written a couple of these tests previously on other controllers and it works just fine. But on this more complex controller, no matter which function I test it on, the .callFake using $q is not entering into the .then block and so no logic in there is being executed at all. I know the controller works for sure as this is on a production website. So why doesn't my test even enter the .then block when I've returned the deferred promise?
Controller Code -
(function() {
'use strict';
angular
.module('myApp')
.controller('EntryAddController', EntryAddController);
function EntryAddController($scope, $state, moment, $sanitize, EntryFactory, $modal, toastr, $log) {
var vm = this;
var now = moment();
var $currentYear = now.year();
vm.queues = [];
vm.calculating = false;
vm.total = 0;
vm.saving = false;
vm.addTxt = 'Confirm Entry';
var tomorrow = now.add(1, 'days').toDate();
var to = now.subtract(1, 'days').add(12, 'months').toDate();
vm.fromDate = tomorrow;
vm.toDate = to;
activate();
////////////////
function activate() {
var queueCache = {};
vm.updateEntrys = function() {
var payload = {
'from': moment(vm.fromDate).format('MM/DD/YYYY'),
'to': moment(vm.toDate).format('MM/DD/YYYY'),
'freq': vm.frequency.value,
'total': vm.total_amount
};
var key = JSON.stringify(payload);
if (!(key in queueCache)) {
EntryFactory.getEntryQueue(payload)
.then(function(resp) {
//LOGIC HERE BUT TEST NEVER ENTERS HERE DESPITE USING $Q
});
} else {
vm.queues = queueCache[key].queue;
vm.total = queueCache[key].total;
vm.calculating = false;
}
}
}
}
})();
Test Code
(function() {
'use strict';
describe('Entry Add Controller Spec', function(){
var vm;
var $controller;
var $q;
var $rootScope;
var EntryFactory;
var $scope;
var toastr;
beforeEach(module('myApp'));
beforeEach(inject(function(_$controller_, _$q_, _$rootScope_, _EntryFactory_) {
$controller = _$controller_;
$rootScope = _$rootScope_;
$scope = _$rootScope_.$new();
$q = _$q_;
EntryFactory = _EntryFactory_;
vm = $controller('EntryAddController', { $scope: $scope });
}));
it('expect EntryFactory.getEntryQueue to correctly set queues and total upon successful response', function() {
var payload = "blah";
var resp = {
"data": 1
}
spyOn(EntryFactory, 'getEntryQueue').and.callFake(function(payload) {
var deferred = $q.defer();
deferred.resolve(resp);
return deferred.promise;
});
EntryFactory.getEntryQueue(payload);
$rootScope.$apply();
//expect logic in .then to have worked
});
});
})();
Edit
Just thought of something... is this because I'm calling the factory function (EntryFactory.getEntryQueue) directly in the test, instead of calling the vm.updateEntrys function around it and therefore it doesn't ever proceed onto the .then portion of the code?
You allways need to consider what your SUT (System/Software under test) is.
In this case it is a controller's component method vm.updateEntrys.
It calls your Mocked EntryFactory.getEntryQueue() method. Also your code is not complete (where do you define vm.frequency object?). Nevertheless I have tested this method with the following test :
/*property
$apply, $log, $modal, $new, $sanitize, $scope, $state, EntryFactory, and,
callFake, data, defer, getEntryQueue, moment, promise, resolve, toastr,
updateEntrys
*/
(function () {
'use strict';
describe('Entry Add Controller Spec', function () {
var vm;
var $controller;
var $q;
var $rootScope;
var EntryFactory;
var $scope;
var toastr;
beforeEach(module('app.controllers'));
beforeEach(inject(function (_$controller_, _$q_, _$rootScope_) {
$controller = _$controller_;
$rootScope = _$rootScope_;
$q = _$q_;
}));
beforeEach(function () {
$scope = $rootScope.$new();
EntryFactory = {
getEntryQueue: function () {
}
};
moment = function () {
return {
format: function () {
}
}
}
vm = $controller('EntryAddController', { '$scope': $scope, '$state': {}, 'moment': moment, '$sanitize': {}, 'EntryFactory': EntryFactory, '$modal': {}, 'toastr': {}, '$log': {} });
});
it('expect EntryFactory.getEntryQueue to correctly set queues and total upon successful response', function () {
var payload = "blah";
var resp = {
"data": 1
};
spyOn(EntryFactory, 'getEntryQueue').and.callFake(function (payload) {
var deferred = $q.defer();
deferred.resolve(resp);
return deferred.promise;
});
vm.updateEntrys();
EntryFactory.getEntryQueue(payload);
$scope.$apply();
//expect logic in .then to have worked
});
});
})();
Related
I have the next 'problem' with Angular 1.
I have this Factory that I use to get the data for the current logged user:
angular.module('myModule')
.factory('authFactory', function ($http, $rootScope, Session, api, backend_url) {
var authFactory = this;
var user = {};
authFactory.init = function(){
// This API returns the information of the current user
api.current_user.get({}).$promise.then(function(res){
user = res;
});
}
// I use this function to return the user
authFactory.user = function () {
return user;
};
}
This is a basic Controller example where I'm trying to access the information retrieved by the above factory:
angular.module('myModule.mypage')
.controller('PageCtrl', function ($scope, authFactory) {
$scope.user = authFactory.user();
authFactory.init();
angular.element(document).ready(function () {
// This will return {} because it's called
// before the factory updates user value
console.log(authFactory.user());
console.log($scope.user);
});
});
The problem is that $scope.user = myFactory.user(); is not being updated once the Factory retrieve the user value.
I think my issue is related with myFactory.user();. I'm using a function, so the value returned by the function is not updated after myFactory.user has changed, I think that's why on PageCtrl the variable $scope.user is not getting any value.
My questions are:
Which is the best approach on my controller to wait until the user info is loaded by authFactory ?
Should I use a service instead ?
Problem with your implementation is that user is being initialized when authFactory.init() is invoked using presumably asynchronous API.
I would suggest you to return promise from authFactory.user method.
angular.module('myModule')
.factory('authFactory', function ($http, $rootScope, Session, api, $q, backend_url) {
var authFactory = this;
var user = {};
authFactory.init = function () {
// This API returns the information of the current user
return api.current_user.get({}).$promise.then(function (res) {
user = res;
});
}
//Return promise from the method
authFactory.user = function () {
var deferred = $q.defer();
if (angular.isDefined(user)) {
deferred.resolve(user);
} else {
authFactory.init().then(function () {
deferred.resolve(user);
});
}
return deferred.promise;
};
});
Then modify controller
angular.module('myModule.mypage')
.controller('PageCtrl', function ($scope, authFactory) {
authFactory.user().then(function (user) {
$scope.user = user;
})
});
angular.module('myModule')
.factory('authFactory', function ($http, $rootScope, Session, api, backend_url) {
var authFactory = this;
authFactory.user = {}
// I use this function to return the user
authFactory.getUser() = function () {
return api.current_user.get({}).$promise.then(function(res){
authFactory.user = res;
});
};
}
angular.module('myModule.mypage')
.controller('PageCtrl', function ($scope, authFactory) {
authFactory.getUser().then(function() {
$scope.user = authFactory.user;
});
});
Provide us a JSFiddle, I tried to help you without any testing environment.
I want to test whether the callback function of $interval is getting called or not after a certain time interval. But I am getting the argument list as empty. I don't know why.
Below is the code which contains $interval -
(function() {
"use strict";
var app = angular.module("AppModule");
app.controller("cntrl", cntrl);
/*#ngInject*/
function cntrl($scope, $state, $interval, utils) {
var vm = this,
data,
timer,
i;
init();
function init() {
_createList();
}
/*Refreshing list after each 30 second interval*/
timer = $interval(function() {
utils.getObjectList()
.then(function(data) {
utils.data = data;
});
init();
}, 1000);
/*Cancelling interval when scope is destroy*/
$scope.$on('$destroy', function() {
$interval.cancel(timer);
});
function _createList() {
//some code
}
}
})();
Below is the unit testing for it -
describe.only("Unit Controller: cntrl", function() {
"use strict";
// Angular injectables
var $q, $httpBackend, $injector, $controller, $rootScope, $state, $location, $templateCache, $compile, $interval;
// Module defined (non-Angular) injectables
var $scope;
// Local variables used for testing
var vm;
// Initialize modules
beforeEach(function() {
module("AppModule");
});
beforeEach(function() {
var templateHtml;
inject(function(_$q_, _$controller_, _$rootScope_, _$state_, _$location_, _$templateCache_, _$compile_, _$interval_) {
$q = _$q_;
$controller = _$controller_;
$rootScope = _$rootScope_;
$location = _$location_;
$state = _$state_;
$templateCache = _$templateCache_;
$compile = _$compile_;
$interval = _$interval_;
$scope = $rootScope.$new();
templateHtml = $templateCache.get("/modules/list.html");
$compile(templateHtml)($scope);
});
});
beforeEach(function() {
vm = $controller("cntrl", {
$scope: $scope
});
});
it("Should call the list API continuosly after a certain interval", function() {
var fn = function() {
utils.getObjectList()
.then(function(data) {
utils.data = data;
});
init();
};
var clock = sinon.useFakeTimers();
var mySpy = sinon.spy($interval);
var throttled = throttle(mySpy);
throttled();
clock.tick(999);
console.log(mySpy);
expect(mySpy.notCalled).to.be.true;
clock.tick(1);
expect(mySpy.called).to.be.true;
expect(intervalSpy.calledWith(fn)).to.be.true; //This is failing
console.log(intervalSpy.getCall(0).args); //this shows empty.
function throttle(callback) {
var timer;
return function() {
clearTimeout(timer);
var args = [].slice.call(arguments);
timer = setTimeout(function() {
callback.apply(this, args);
}, 1000);
};
}
});
});
I have a Karma test spec like this:
'use strict';
describe('Controller: RegistrationCtrl', function () {
beforeEach(module('stageApp'));
var RegistrationCtrl, scope;
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
RegistrationCtrl = $controller('RegistrationCtrl', {
$scope: scope
});
}));
it('should exist', function () {
expect(RegistrationCtrl.init).toBeDefined();
});
it('should exist', function () {
expect(scope.testPassword).toBeDefined();
});
});
I'm trying to access the init function in my controller that was declared like this:
var init = function () {
$scope.showProfileFeatures = false;
};
My test for scope.testPassword works just fine, the test for init() fails. In addition to trying it with RegistrationCtrl.init, I've also tried just 'init' and that fails as well.
here is the working test password function in my controller:
$scope.testPassword = function ($event) {
var password = $scope.regPassword;
if (password.length < 8) {
alert("bad password");
}
};
init is not a public function on your controller. You need to do $scope.init whereas you have var init = ...
I have an angular factory that makes an $http call with a get and then.
.factory('DataModel', function($http) {
I have a .get.then that works great. The value comes back, and since I originally returned a function to return the factory value, everything updates when it changes.
Now I have to make a dependent call based on the data that returned the first time.
First try: $http.get.then inside the outer $http.get.then.
The inner (dependent) call successfully gets the data, but when it updates the factory parameters only the first .get.then is picked up by the calling controller.
Next try: $scope.$watch.
angular.module('starter.services', [])
.factory('DataModel', function($scope, $http) {
If I put a $scope parameter in there I get an error:
Unknown provider: $scopeProvider <- $scope <- DataModel
So I can't seem to use the $scope.$watch method.
Third try: callbacks?
I'm afraid that if I use a callback approach I'll get the data back, but it won't update just like my nested get.then. didn't update.
Here is my full factory:
angular.module('starter.services', [])
.factory('DataModel', function($http) {
var days = {};
var todaysFlavorIndex = 32;
var todaysFlavorName = [32, 'Loading ...', "vanilla_chocolate_chip.jpg"];
var daysLeftCalendar = [];
var flavors = [];
// calendar objects
$http.get("https://jsonblob.com/api/5544b8667856ef9baaac1")
.then(function(response) {
var result = response.data;
days = result.Days;
var dateObj = new Date();
var day = dateObj.getDate();
var endOfMonthDate = new Date(new Date().getFullYear(), dateObj.getMonth(), 0).getDate();
for (var di = day; di <= endOfMonthDate; di++) {
var flavor = days[di - 1];
daysLeftCalendar.push(flavor[1]);
}
var todaysFlavorIndex = -1;
// $scope.$watch('todaysFlavorIndex', function() {
// // Http request goes here
// alert('updating !');
// });
for (var i = 0; i < days.length; i++) {
if ((days[i])[0] == day) {
todaysFlavorIndex = (days[i])[1];
}
}
// flavors
$http.get("https://jsonblob.com/api/55450c5658d3aef9baac1a")
.then(function(resp) {
flavors = resp.data.flavors;
todaysFlavorName = flavors[todaysFlavorIndex];
});
}); // end then
return {
getDays: function() {
return days;
},
getMonth: function() {
return days;
},
getFlavors: function() {
return flavors;
},
getTodaysFlavorIndex: function() {
return todaysFlavorIndex;
},
getTodaysFlavorName: function() {
return todaysFlavorName; // flavors[todaysFlavorIndex];
},
today: function() {
var dateObj = new Date();
var day = dateObj.getUTCDate();
return todaysFlavorIndex;
},
remainingFlavorIndexes: function() {
return daysLeftCalendar
}
};
})
Firstly , services has no $scope.
So injecting scope in factory will always throw you exceptions.
Secondly , try to catch callback from controller instead of factory
Try like this
angular.module('starter.services', [])
.factory('DataModel', function($http) {
return {
myFunction: function() {
return $http.get("https://jsonblob.com/api/5544b8667856ef9baaac1");
}
}
})
.controller("myCtrl", function($scope, DataModel) {
DataModel.myFunction().then(function(result) {
// success
// put your code here
}, function(e) {
// error
});
})
Thirdly, If you wanna have inner $http you can use $q
Try like this
angular.module('starter.services', [])
.factory('DataModel', function($http) {
return {
myFunction: function() {
return $http.get("https://jsonblob.com/api/5544b8667856ef9baaac1");
},
myFunction2: function() {
return $http.get("https://jsonblob.com/api/55450c5658d3aef9baac1a");
}
}
})
.controller("myCtrl", function($scope, DataModel, $q) {
$q.all([
DataModel.myFunction(),
DataModel.myFunction2()
]).then(function(data) {
console.log(data[0]); // data from myFunction
console.log(data[1]); // data from myFunction2
});
});
Normally, I created a controller which used $scope syntax, so, I could pass a current $scope to a isolated scope of the modal directive as following:
$scope.openEditModal = function() {
$scope.modalInstance = $modal.open({
templateUrl: 'views/budgets/mainbudgets/edit',
scope: $scope // Passing a $scope variable
});
$scope.modalInstance.close();
};
However, I just switched the controller to use this syntax:
var self = this;
// User edit modal
this.openEditModal = function() {
self.modalInstance = $modal.open({
templateUrl: 'views/budgets/mainbudgets/edit',
scope: self; // This doesn't work
});
self.modalInstance.close();
};
So, how can I pass a current this to be used in isolated scope of the modal directive?
EDIT
Here is the whole code of my controller:
angular.module('sms.budgets').controller('BudgetsMainController', ['$scope', 'Global', '$modal', '$timeout', '$rootScope','Budgets', function($scope, Global, $modal, $timeout, $rootScope,Budgets) {
var self = this;
this.newBudget = {};
this.budgets = [];
function init() {
var data = {};
// Load main budget from DB
Budgets.load('main-budgets').success(function(budgets) {
self.budgets = budgets || [];
});
}
init();
/**
* Create a new main budget
*/
this.create = function() {
var data = {};
data.budget = self.newBudget;
data.dbName = 'Budget';
Budgets.create('budgets', data).success(function() {
self.isSuccess = true;
$timeout(function() {
self.isSuccess = false;
}, 5000);
}).error(function(err) {
self.isError = true;
$timeout(function() {
self.isError = false;
}, 5000);
});
};
this.edit = function() {
self.modalInstance.close();
};
// User edit modal
this.openEditModal = function() {
var newScope = $rootScope.$new();
newScope.modalInstance = self.modalInstance;
newScope.edit = self.edit;
self.modalInstance = $modal.open({
templateUrl: 'views/budgets/mainbudgets/edit',
scope: newScope
});
self.modalInstance.close();
};
this.cancelEditModal = function() {
self.modalInstance.dismiss('cancel');
};
}]);
You can't use this as a scope. They are different. Since $scope is a internal variable of AngularJS you have to keep it as it.
To show that, I've created a Plunkr (open the console and see the diffence between this and $scope)
http://plnkr.co/edit/DkWQk4?p=preview
Anyway, is a good practise to use a different scope on the modal controller. Here you have an example showing how to communicate between the main controller and the modal controller:
From the MainCtrl:
var modalInstance = $modal.open({
templateUrl: 'views/parts/modalUrlImg.html',
controller: 'ModalUrlCtrl',
resolve: {
url: function () { // pass the url to the modal controller
return $scope.imageUrl;
}
}
});
// when the modal is closed, get the url param
modalInstance.result.then(function (url) {
$scope.courses[i].imageUrl = url;
});
From the Modal Ctrl:
.controller('ModalUrlCtrl', function($scope, $modalInstance, url) {
$scope.url = url; // get the url from the params
$scope.Save = function () {
$modalInstance.close($scope.url);
};
$scope.Cancel = function () {
$modalInstance.dismiss('cancel');
};
$scope.Clean = function () {
$scope.url = '';
};
});
Hope this help you, cheers.
--- EDIT ---
You can keep the controller as syntax. In fact, you must mix both syntax, since you can only use this to add vars and functions, but not tu access other scope things, such as $scope.$on...
So, to do that in your case, just pass $scope:
var self = this;
// User edit modal
this.openEditModal = function() {
self.modalInstance = $modal.open({
templateUrl: 'views/budgets/mainbudgets/edit',
scope: $scope;
});
self.modalInstance.close();
};
I've tried in the updated plunkr and it's working now:
http://plnkr.co/edit/DkWQk4?p=preview
$scope != controller
By passing this to $modal.open() you are passing the reference of the controller and not the scope. Try passing $scope instead.