Angular controller undefined within jasmine test - javascript

I'm currently having an issue writing some tests for a controller. Within the beforeEach block below I need to instantiate an activityController and inject the scope object. I have added a console log before the call to the $controller service and this is outputted however the one after never gets called therefore something is breaking within the $controller block.
beforeEach(inject(function($controller) {
console.log(activityController);
activityController = $controller('activityController', {
'$scope': $scope
});
console.log("TEST");
}));
Within my tests I'm seeing Type Error: activityController is undefined in C:\.......\activity.controller.test.js so I know it's definitely not being instantiated.
I've created a gist of the relevant files here: https://gist.github.com/junderhill/e181ce866ab1ebb1f805
The activity controller not being instantiated correctly is causing my tests to fail. Any ideas on what may be causing this would be appreciated. Thanks

Jason.
Try to set activityService whilst creating controller, because you should also inject all services.

Looks like this line may be causing the problem:
mockRoleService.setCurrentRole({"AssignmentID":21,"EndDate":"2049-12-31T00:00:00","StartDate":"2000-01-01T00:00:00","UserType":1,"AccessLevel":"00000000-0000-0000-0000-000000000000","Description":"Demonstration Territory 1","TeamID":null});
It looks like you're using an actual injected version of your roleService instead of a stubbed literal, so it's actually going to fire off your implementation, which is...
this.setCurrentRole = function(role){
currentRole = role;
$http.get("http://localhost:14938/api/User/GetTeamForAssignment?assignmentId=" + role["AssignmentID"] + "&assignmentType=" + role["UserType"])
.success(function (data) {
currentTeam = data;
});
}
If you're going to use that service directly with an $httpBackend mock, I'd actually wrap that operation in a $q.defer(), because currently as that stands, that is an asychronous call. You'll want that operation to complete to set the currentTeam properly. So, maybe something like..
this.setCurrentRole = function(role){
var deferred = $q.defer();
currentRole = role;
$http.get("http://localhost:14938/api/User/GetTeamForAssignment?assignmentId=" + role["AssignmentID"] + "&assignmentType=" + role["UserType"])
.success(function (data) {
currentTeam = data;
deferred.resolve();
});
return deferred.promise;
}
And obviously do a deferred.reject of some sort if something wonky comes back from HTTP.
Hope that helps!
Eric

Related

Angular Translate async timing issue with $translateProvider.useStaticFilesLoader

I am using the excellent Angular Translate ($translate) directive/service to deal with multiple Locale Languages and since I have multiple locale files I use the convenient $translateProvider.useStaticFilesLoader to load my translation files through a structure of localeAbbr.json, for example en.json, es.json, etc... I built a Plunker to show my open source project and that project uses the locale through Git raw files (pointing to the actual Github repository, meaning not local to the plunker demo). My project is built as a Directive and a Service, I made a small Plunker to show my timing issue with the JSON file loading.
All that to say that it seems $translateProvider.useStaticFilesLoader works asynchronous while I would really need it to be synchronous because by the time the plunker runs, the JSON files are not yet parsed while I already called a $translate.instant() on my messages.
I have a Plunker showing the problem.
And here is part of my quick Service demo:
app.factory('validationService', ['$filter', '$translate', function ($filter, $translate) {
var service = this;
var validationSummary = [];
var errorMessages = [
'INVALID_ALPHA',
'INVALID_ALPHA_SPACE',
'INVALID_ALPHA_NUM',
'INVALID_BOOLEAN'
];
//var $translate = $filter('translate');
for(var i=0, ln=errorMessages.length; i < ln; i++) {
validationSummary.push({
field: i,
message: $translate.instant(errorMessages[i])
});
}
// attach public functions
service.getValidationSummary = getValidationSummary;
return service;
// function declaration
function getValidationSummary() {
return validationSummary;
}
}]);
The $translateProvider configuration
app.config(['$translateProvider', function ($translateProvider) {
$translateProvider.useStaticFilesLoader({
prefix: 'https://rawgit.com/ghiscoding/angular-validation/master/locales/validation/',
suffix: '.json'
});
// load English ('en') table on startup
$translateProvider.preferredLanguage('en').fallbackLanguage('en');
}]);
Call my Service through the Controller:
app.controller("TestController", function($scope, validationService) {
var vm = this;
vm.displayValidationSummary = true;
vm.validationSummary = validationService.getValidationSummary();
});
and finally the HTML using the controller:
<div class="alert alert-danger alert-dismissable" ng-show="vm.displayValidationSummary">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true" ng-click="displayValidationSummary = false">×</button>
<h4><strong>{{ 'ERRORS' | translate }}!</strong></h4>
<ul>
<li ng-repeat="item in vm.validationSummary">{{item.field }}: {{item.message}}</li>
</ul>
</div>
Since I'm using AngularJS 1.3+, I also found that $translate only gets translated once, so the author suggest to use translateFilter.$stateful = true; and I tried but that doesn't seem to help.
Again here is the Plunker
I have been spending weeks on trying to find and code all kind of solution but I never got it to work and I'm really sad of seeing my raw translation code :(
Please Help!!!
EDIT
I realized that my question was not covering everything related to my problem. On top of the translation delay problem, I also have to pass extra arguments and that is a huge problem passing them to the translation anonymous function. By the time the promise is finished, the state of my arguments have already changed. For example:
$translate(validator.message).then(function(translation) {
// only log the invalid message in the $validationSummary
addToValidationSummary(formElmObj, translation);
// error Display
if(!isValid) {
updateErrorMsg(translation, isValid);
}else if(!!formElmObj && formElmObj.isValid) {
addToValidationSummary(formElmObj, '');
}
}, function(data) {
throw 'Failed to translate' + data;
});
When working with AngularJS, or JavaScript for that matter you really need to embrace the asynchronous paradigm. In order to make dealing with asynchronous code less cumbersome you can employ the use of Promises. Angular gives you a service called $q which does the heavy lifting for you
https://docs.angularjs.org/api/ng/service/$q
getting ones head around Promises can take time but well worth the effort in the long run.
Essentially what you need to do with your validationService is to make use of $translate's promise api which will give you the translation you require based on the supplied key when it is in a position to do so. What this boils down to is that you ask $translate for all of the translationId's you wish to get a translation for and when all have been fetched you populate the validationSummary array with your messages.
app.factory('validationService', ['$q', '$translate', function ($q, $translate) {
var translationsPromises = [],
validationSummary = [],
errorMessages = [
'INVALID_ALPHA',
'INVALID_ALPHA_SPACE',
'INVALID_ALPHA_NUM',
'INVALID_BOOLEAN'
];
angular.forEach(errorMessages, function(val, key) {
translationsPromises.push($translate(val));
});
$q.all(translationsPromises)
.then(function(translations) {
angular.forEach(translations, function(val, key) {
validationSummary.push({
filed: key,
message: val
});
});
})
.catch(function (err) {
console.error('Failed to translate error messages for validation summary', err);
});
// function declaration
function getValidationSummary() {
return validationSummary;
}
return {
getValidationSummary: getValidationSummary
};
}]);
I've forked your plunker and modified it to include the above sample
http://plnkr.co/edit/7DCwvY9jloXwfetKtcDA?p=preview
Another observation is that you are using the translate filter in the HTML. Please be aware that this can prove to be expensive if you have a large DOM as Angular will make the call to translate each key on every digest. An approach to consider would be to provide your vm with a labels object and use the $filter service to populate them upon controller instantiation.
I found out the answer to my problem of passing extra arguments to the anonymous function of the promise is to use Closures, in this way the variables are the same before the promise and inside it too. So I basically have to wrap my $translate call into the closure, something like the following:
(function(formElmObj, isValid, validator) {
$translate(validator.message).then(function(translation) {
message = message.trim();
// only log the invalid message in the $validationSummary
addToValidationSummary(formElmObj, message);
// error Display
if(!isValid) {
updateErrorMsg(message, isValid);
}else if(!!formElmObj && formElmObj.isValid) {
addToValidationSummary(formElmObj, '');
}
}, function(data) {
throw 'Failed to translate' + data;
});
})(formElmObj, isValid, validator);
and now finally, my variables are correct and keep the value at that point in time :)
While it is true that $translateProvider.useStaticFilesLoader does not return a promise, I looked inside the $translate service and found that it provides a handy callback onReady() which does return a promise. This callback is invoked when the $translate service has finished loading the currently selected language, and is useful for making sure that instant translations will work as expected after page initialization:
$translate.onReady(function () {
// perform your instant translations here
var translatedMsg = $translate.instant('INVALID_ALPHA');
});

Injecting service which define by module.provider

Sorry to ask something like this, but i m already spend couple of days to solve this one.
But i m really need help. I am already read Understanding Dependency Injection
and also other stack question use $http inside custom provider in app config use the
$injector
on run block also not work in my code.
Perhaps someone here can help me, i am also worried how annotate the injection on my provider for minification.
My code in
Here
The problem is that when you call $http in the _getItem you are in the "controller". A solution could be to do this:
this.$get = function($http) {
return {
mainUrl : _mainUrl,
getItem : _getItem,
httpWrapper : $http
};
And then in the _getItem assign (or use the httpWrapper directly):
var $http = this.httpWrapper;
In order to avoid problems with the minification, you could add the injection to the method $get:
this.$get = ['$http', function($http) {
Here the updated Plunker: http://plnkr.co/edit/m7oXNolo6iV8Zy1NJ0vx
EDIT: I think that what you wanted to achieve is this:
var _getItem = function() {
var _d = null;
console.log(this)
var $http = this.httpWrapper;
var $q = this.qWrapper;
_d = $q.defer();
$http.get(_mainUrl)
.success(function(data) {
console.log(data);
_d.resolve( {data: data, x: "efe"} );
})
.error(function(status) {
_d.resolve( status );
});
return _d.promise;
};
$q.defer() allows to defer the http call to your controller, then on your controller you have to edit this:
facade.getItem().then(function(data){
$scope.myReturn = data;
}
Updated Plunker: http://plnkr.co/edit/xR1d3KnWhX51jHHZ8t3m
Note: Consider that the nature of then is asynchronous, thus if you use the variable straight afterwards, it still will be undefined

AngularJS learning about services and factories, a service in a factory?

I'm really trying to wrap my head around angular's service/factory/provider constructs before I refactor a pretty big project.
I've read lots of docs and articles on services vs. factories and thought I understood how each of them are created and what they do.
However, while trying stuff I attempted to use a service in a factory or two...
This was really useful: I now understand that there is only one of my 'jsonService' (it's a singleton), so this simple approach WILL NOT WORK... (I'll need each factory to have a separate instance of something)
.service('jsonService', ['$http', function ($http) {
var data= {'msg':'no data'};
this.serviceData= data;
this.get= function(url){
$http.get(url).then(function (resp) {
data= resp.data;
});
}
}])
.factory('fac1', ['jsonService', function(jsonService){
jsonService.get('json/data1.json');
return jsonService.serviceData;
}])
.factory('fac2', ['jsonService', function(jsonService){
jsonService.get('json/data2.json');
return jsonService;
}])
When I use the factories in a controller like:
myController.f1= fac1;
myController.f2= fac2.serviceData;
I can see that fac1 and fac2 both return the same object, they both have {msg:'no data'}, if I change one then they both change.
My question is:
Even though I can break on the service and see data= {msg:'no data'} and see it being set to the response data - why do I not see any change in fac1 or fac2 ?
All I can think is that somewhere there must be more than one var data, something is not a 'singleton' ????
EDIT: I have now tried:
this.serviceData= function(){return data;};
and:
myController.f2= fac2.serviceData(); // this is always the 'no data' object
myController.f3= fac2.serviceData;
if I then (a long time later) call:
var something= myController.f3();
then I do get the json data... but myController.f2 is still {msg:'no data'} why ?
Ok, after trying m.e.conroy's suggestion I finally figured it out...
The problem is nothing to do with angular, it's how javascript passes objects.
(see: Is JavaScript a pass-by-reference or pass-by-value language? )
The factories passed back a 'copy-reference' of the original {msg:'no data'} object, and when the service eventually assigned:
data= resp.data;
that replaced 'data', but the factory-supplied references persist as the old object.
Now, if I do:
.service('jsonService', ['$http', function ($http) {
var data= {'msg':'no data', 'result':null};
this.serviceData= data;
this.get= function(url){
$http.get(url).then(function (resp) {
data.result= resp.data; // update properties of the data object
data.msg='got data!'; // instead of replacing it
});
}
}])
...then everything makes sense!
The variables in myController are changed when data arrives (since I have not 'swapped out' the data object).
Obviously, I still have the problem that my two factories return the same object (I'll look at Sacho's suggestion on this) - but I think I've learned something pretty fundamental here.
Try this:
this.serviceData = function(){
return data;
};
Also you probably have a race situation. $http may not have returned by the time the application asks for the serviceData
The difference between angular's "services" and "factories" is miniscule - I suggest just using one or the other, and sticking with it. From this point out, I'll refer to these elements as "services", even though I exclusively use angular.factory to declare them.
For what you ultimately want to do, there is a much simpler solution - simply return the promise from your service.
this.get= function(url){
return $http.get(url)
}
Then in your controller/directive/whatever, just use it like:
jsonService.get(url).then(function success(response) {
// do things with the response
})
But you seem to want to use a service to create many instances, so here's a contrived example achieving that:
.factory('jsonService', jsonService)
jsonService.$inject = ['$http']
function jsonService($http) {
return Fixed
function Fixed(url) {
this.url = url
this.promise = null
this.get = get.bind(this)
function get() {
// This caches the request so you only do it once, for example
if (this.promise == null) {
this.promise = $http.get(url)
}
return this.promise
}
}
}
Then in your controller, you would do something like:
var fixedOne = new jsonService('fixedOne')
fixedOne.get().then(...)

Can promises be used to ensure data from AJAX request is not used before the request completes?

I am writing an app in AngularJs, and I am having some conceptual difficulties regarding promises. More specifically, the role of promises in asynchronously pulling data from an api.
When a user loads the app, the following should happen:
Send AJAX request to api.
Render View.
Receive response from api.
Do stuff with data.
However since the AJAX request (by definition) is asynchronous, the app tries to do stuff with the data before it is returned by the server, which causes object is undefined errors and the like.
At the I have a service something like this
app.service('bookService', ['$http', function($http){
var someData;
$http.get('Some URL').success(function (data){
someData = data.someData;
});
var bookService = {};
bookService.getSomeData = function (){
return someData;
};
return bookService;
}]);
And a controller like this
app.controller('BookController', ['bookService', function(bookService){
console.log(bookService.getSomeData().property);
}]);
The controller loads as soon as the attached view is displayed, and attempts to do something with the someData object. At that point it is still undefined (the http request has not yet returned) an errors occur.
Instead I though something like this might work :
app.service('bookService', ['$http', function($http){
var someDataPromise = $http.get('Some URL');
var bookService = {};
bookService.getSomeData = function (){
someDataPromise.success(function (data){
// This should return the data contained by the promise to the caller
// of the bookService.getSomeData function.
});
};
return bookService;
}]);
The controller would be as before.
My experience with promises, and JavaScript as whole (especially concepts like callbacks and anonymous functions) are quite weak. My suspicion is that, as concept, this probably works, although probably not in the way I have showed here.
Can promises be used in the way that I have shown here, or am I lacking in understanding as to what is going on here?
You should consider changing your service to return a promise as well:
app.service('bookService', ['$http', function($http){
var bookService = {};
bookService.getSomeData = function (){
return $http.get('Some URL');
});
return bookService;
}]);
then your controller can use promises too:
app.controller('BookController', ['bookService', '$scope', function(bookService, $scope){
bookService.getSomeData().success(function(data) {
$scope.data = data;
});
}]);
Your service would usually do a bit more than just retrieve the data but hopefully this demonstrates the concept.
It's worth noting that Angular's data binding will pick up the data as soon as you set it on the scope.

How to mock $window.location.replace in AngularJS unit test?

I've got the following service:
angular.module("services")
.factory("whatever", function($window) {
return {
redirect: function() {
$window.location.replace("http://www.whatever.com");
}
};
});
How to mock $window object in unit test to prevent reloading the page when running tests?
I tried using
spyOn($window.location, 'replace').andReturn(true);
, but it didn't work (still got "Some of your tests did a full page reload!" error) and
$provide.value('$window', {location: {replace: jasmine.createSpy()}})
, but I was getting an error (Error: [ng:areq] Argument 'fn' is not a function, got Object) with stack trace pointing only to angular own source, so it wasn't very helpful...
In Chrome (didn't test inother browsers), location.replace is readonly so spyOn wasn't able to replace it.
$provide.value should work. Something must be wrong somewhere in your code.
Here is a working unit test
describe('whatever', function() {
var $window, whatever;
beforeEach(module('services'));
beforeEach(function() {
$window = {location: { replace: jasmine.createSpy()} };
module(function($provide) {
$provide.value('$window', $window);
});
inject(function($injector) {
whatever = $injector.get('whatever');
});
});
it('replace redirects to http://www.whatever.com', function() {
whatever.redirect();
expect($window.location.replace).toHaveBeenCalledWith('http://www.whatever.com');
});
});
I'm going with an easier but perhaps less elegant solution. I'm writing a wrapper for $window.location, which I can then mock. Relating that to your code, I'd be mocking the whatever.redirect function, rather than mocking $window (I'm assuming here that your real function is more complex).
So I'd end up with:
angular.module("services")
.factory("whatever", function($window) {
return {
do_stuff_that_redirects: function() {
lots of code;
this.redirect("http://www.whatever.com");
maybe_more_code_maybe_not;
},
redirect: function(url) {
$window.location.replace(url);
}
};
});
I can then directly mock the redirect method, and just trust that since it's only one line of code it can't really go wrong.
spyOn(whatever, 'redirect').andCallFake(function(){});
expect(whatever.redirect).toHaveBeenCalledWith('http:/my.expected/url');
This is sufficient for my purposes, and lets me validate the url called.
I'll offer another approach that might work for you. I faced the same problem while unit testing a controller 'action' that ultimately redirects the user (full-page-load, but to a different page in the larger website/application). To give some context, the controller fires off an AJAX request, and if the response is OK, it will redirect the user to a different page via $window.location.replace():
$http.post('save', data)
.success(function(responseData, status, headers, config) {
if(responseData.redirect) {
$window.location.replace(responseData.redirect);
}
})
.error(function(responseData, status, headers, config) {
console.error("ERROR while trying to create the Event!!");
});
The test for this controller function caused the same "Some of your tests did a full page reload!" error. So I added the following to the beforeEach() function for the controller spec, to mock out the $window service:
mockWindow = { location: { replace: function(url) { console.log('redirecting to: ' + url); } } };
eventCtrl = $controller('EventCtrl', { $scope: scope, $window: mockWindow });
Of course, this solution prevents me from (cleanly) verifying that the replace function was called with an expected argument, but I don't really care about that right now.... Hope that helps.
I think what you want is to use the $location service, rather then calling $window.location. There is also a whole page explaining this feature here: http://docs.angularjs.org/guide/dev_guide.services.$location.
Using this, it should be fairly simple to use a stubbed version of the $location service in you tests.
$location.path('/someNewPath');
$location.replace();
// or you can chain these as: $location.path('/someNewPath').replace();

Categories

Resources