AngularFire karma test never reaches promise callback - javascript

I have a controller that uses AngularFire's $firebaseObject.
When the controller initializes, it sets the reference to Firebase, and uses $loaded function to verify that data from Firebase is actually there. Here is a simplified code of the controller:
myModule.controller('VotingQuestionsCtrl', ['$scope', '$rootScope', '$firebaseObject',
function($scope, $rootScope, $firebaseObject) {
var baseRef = new Firebase("https://somefirebaserepo.firebaseio.com/pathtomyobjects"),
topicsRef = baseRef.child("topics");
var topics = $firebaseObject(topicsRef);
$scope.dataLoaded = false;
topics.$loaded().then(function() {
$scope.dataLoaded = true;
topics.$bindTo($scope, 'topics').then(function() {
//do some stuff
});
}, function(error) {
$scope.dataLoaded = true;
console.log("oh no!");
});
This controller works pretty good in real life. The problem is testing it with Karma. No matter what I tried, my test spec never reaches the callback for topics.$loaded (neither the success function nor the error function).
Here is the current state of my test spec:
describe('VotingQuestionsCtrl', function() {
var $controller, $scope, $rootScope, $firebaseObject, $stateParams, $q, baseRef, $timeout;
beforeEach(module('wsApp.controllers'));
beforeEach(inject(function ($injector) {
MockFirebase.override();
$controller = $injector.get('$controller');
$q = $injector.get('$q');
$rootScope = $injector.get('$rootScope');
$timeout = $injector.get('$timeout');
$scope = $rootScope.$new();
$firebaseObject = $injector.get('$firebaseObject');
$rootScope.user = {username: "user"};
$rootScope.selectedSim = {phase: "phase1"};
baseRef = new Firebase("https://somefirebaserepo.firebaseio.com/pathtomyobjects");
baseRef.set({topics: {simId: {phase1: "bla"}}, users: {}});
baseRef.flush();
}));
it('loads the controller', function(done) {
$controller('VotingQuestionsCtrl', {$scope: $scope, $rootScope: $rootScope, $firebaseObject: $firebaseObject});
$scope.$digest();
setTimeout(function() {
expect($scope.dataLoaded).toBeTruthy();
done();
}, 200);
});
});
I've tried all sorts of flushing, digesting, whatever... the test goes through the rest of the controller, but the async part just doesn't work.

Related

Jasmine spyOn not working properly on AngularJS directive

I'm working on an AngularJS app and I'm facing some problems with Jasmine's SpyOn in a concrete directive.
The directive is quite simple, just call a service's method and when it resolves/rejects the promise acts in consequence, setting some values or another ones.
The problem: When I try to mock SignatureService.getSignatureData SpyOn does not work as I expect, and acts as if I was invoking jasmine's callThrough method over getSignatureData.
I've been using spyOn and mocks in other directives and services, and there was no problem with those.
I've been trying to solve this issue the last two days, comparing with other solutions and user's answers, but I can not find a valid solution.
Here's my code:
AngularJS directive code:
angular
.module('module_name')
.directive('signatureDirective', signatureDirective);
angular
.module('GenomcareApp_signature')
.controller('signatureDController', signatureDController);
function signatureDirective() {
return {
restrict: 'E',
templateUrl: 'components/signature/signature.directive.html',
controller: signatureDController,
controllerAs: 'ctrl',
bindToController: true
};
}
signatureDController.$inject = [
'$scope',
'$rootScope',
'$location',
'SignatureService'
];
function signatureDController($scope, $rootScope, $location, SignatureService) {
var controller = this;
$scope.$on('pdfFileLoadSuccessfully', function (data) {
console.log(data);
controller.loadPdfSucceed = true;
});
$scope.$on('pdfFileLoadFails', function (data) {
console.error(data);
controller.loadPdfError = true;
});
function loadDirectiveInitData() {
var queryParameters = atob($location.search().data);
controller.email = queryParameters.split(';')[0];
controller.phone = queryParameters.split(';')[1];
controller.docid = queryParameters.split(';')[2];
SignatureService.getSignatureData(controller.email, controller.phone, controller.docid)
.then(
function (data) {
console.log(data);
controller.stampTime = data.stamp_time;
controller.fileUrl = data.original_file.url;
},
function (error) {
console.error(error);
controller.error = true
})
.finally(
function () {
controller.endLoad = true;
})
}
loadDirectiveInitData();
}
Jasmine test code:
'use strict';
/* global loadJSONFixtures */
describe('Test :: Signature directive', function () {
beforeEach(angular.mock.module('app'));
beforeEach(module('translateNoop'));
var $q, $compile, $rootScope, controller, $scope, $httpBackend, $location, SignatureService;
beforeEach(angular.mock.inject(function (_$controller_, _$q_, _$rootScope_, _$location_, _$compile_, _$httpBackend_, _SignatureService_) {
$q = _$q_;
$compile = _$compile_;
$location = _$location_;
$scope = _$rootScope_.$new();
$httpBackend = _$httpBackend_;
SignatureService = _SignatureService_;
spyOn($location, 'search').and.returnValue({data: 'dGVzdEB0ZXN0LmNvbTsrMzQ2NjY2NjY2NjY7WG9TUFFnSkltTWF2'});
$httpBackend.whenGET('components/signature/signature.directive.html').respond(200, '');
controller = _$controller_('signatureDController', {$scope: $scope});
}));
describe('Testing directive', function () {
it('Init data should be set when promise resolves/rejects', function (done) {
// SpyOn DOES NOT MOCK THE SERVICE METHOD
spyOn(SignatureService, 'getSignatureData').and.callFake(function () {
return $q.resolve({...})
});
var element = angular.element('<signature-directive></signature-directive>');
element = $compile(element)($scope);
$scope.$digest();
done();
// ... some expect stuff
});
});
});
If any one can give me some advice or solution, I would be very thankful.
Thank you very much.
UPDATE1: I don't know why, but if I do not declare the controller variable in the global beforeEach, Jasmine's spyOn mocks the method as I expect.
Now the issue is how to get the controller to test that the controller values are set as expected.
Well... I realized that the problem was that the controller was being created before all, and somehow when the service was mocked the controller ignores it.
This idea came by accident, when I paste the service's spyOn in the global beforeEach.
So I decide to create a new instance of the controller and the corresponding spyOn with the desired result inside the beforeEach of each describe.
It works. Maybe it's not the best aproach, and I encourage to anyone who have the answer to post it. I'm going to be eternally greatful.
Here's my final test code:
describe('Test :: Signature directive', function () {
beforeEach(angular.mock.module('app'));
beforeEach(module('translateNoop'));
var $q, $compile, $rootScope, $scope, $httpBackend, $location, SignatureService, test_fixture;
beforeEach(angular.mock.inject(function (_$q_, _$rootScope_, _$location_, _$compile_, _$httpBackend_, _SignatureService_) {
$q = _$q_;
$compile = _$compile_;
$location = _$location_;
$scope = _$rootScope_.$new();
$httpBackend = _$httpBackend_;
SignatureService = _SignatureService_;
// controller = _$controller_;
spyOn($location, 'search').and.returnValue({data: 'dGVzdEB0ZXN0LmNvbTsrMzQ2NjY2NjY2NjY7WG9TUFFnSkltTWF2'});
$httpBackend.whenGET('components/signature/signature.directive.html').respond(200, '');
}));
describe('Testing directive when service resolve promise', function () {
var controller;
beforeEach(inject(function(_$controller_) {
spyOn(SignatureService, 'getSignatureData').and.callFake(function () {
return $q.resolve({...})
});
controller = _$controller_('signatureDController', {$scope: $scope})
}));
it('Init data should be set', function () {
// spyOn($location, 'search').and.callThrough();
var element = angular.element('<signature-directive></signature-directive>');
element = $compile(element)($scope);
$scope.$digest();
// ... some expect(...).toEqual(...) stuff and more
});
});
});
Thank you for your time.
Try to use $q.defer(), here's an example:
it('Init data should be set when promise resolves/rejects', function (done) {
// SpyOn DOES NOT MOCK THE SERVICE METHOD
spyOn(SignatureService, 'getSignatureData').and.callFake(function () {
let deferred = $q.defer();
deferred.resolve({...});
return deferred.promise;
});
var element = angular.element('<signature-directive></signature-directive>');
element = $compile(element)($scope);
$scope.$digest();
done();
// ... some expect stuff
});

How to test $http with Jasmine in AngularJS

I'm trying to test the data received from an $http request in my controller.
I don't have too much experience testing with Angular so I'm struggling to under stand how to do it.
$scope. always comes back undefined and when I've tried fetching the data from the test, that seems to fail also. What am I missing?
Controller:
'use strict';
var myApp = angular.module('myApp.view1', ['ngRoute']);
myApp.config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/view1', {
templateUrl: 'view1/view1.html',
controller: 'View1Ctrl'
});
}]);
myApp.controller('View1Ctrl', [
'$scope',
'$http',
function($scope, $http) {
$http.get('view1/data.json')
.then(function(res){
$scope.data = res.data.data
});
}]);
Test:
'use strict';
describe('myApp.view1 module', function() {
beforeEach(module('myApp.view1'));
describe('view1 controller', function(){
var scope, testCont;
beforeEach(inject(function($rootScope, $controller) {
scope = $rootScope.$new();
testCont = $controller('View1Ctrl', {$scope: scope});
}));
it('should....', function(){
expect($scope.data).toBeDefined();
});
});
});
The HTTP requests will not fire unless you call $httpBackend.flush().
More information can be found here: http://docs.angularjs.org/api/ngMock.$httpBackend
Test:
'use strict';
describe('myApp.view1 module', function() {
var $httpBackend, $rootScope, createController, jsonHandler;
beforeEach(module('myApp.view1'));
describe('view1 controller', function(){
var scope, testCont;
beforeEach(inject(function($rootScope, $controller, $injector) {
// Set up the mock http service responses
$httpBackend = $injector.get('$httpBackend');
// backend definition common for all tests
jsonHandler= $httpBackend.when('GET', '/view1/data.json')
.respond({data: '[XXX,XXX,XXX]'});
// Get hold of a scope (i.e. the root scope)
$rootScope = $injector.get('$rootScope');
// The $controller service is used to create instances of controllers
var $controller = $injector.get('$controller');
createController = function() {
return $controller('View1Ctrl', {'$scope' : $rootScope });
};
}));
it('should....', function(){
$httpBackend.expectGET('/view1/data.json');
var controller = createController();
$httpBackend.flush();
});
});
});

AngularJs Jasmine unable to call $scope method within $rootScope method

I have a method in my controller $scope.get another method $rootScope.search. I am writing the unit tests using Karma and Jasmin.
When I am trying to test the method $rootScope.search it throwing an error, what I am doing wrong?
Code is as follows:
Ctrl.js
App.controller('Ctrl', ['$scope', '$rootScope', function($scope, $rootScope){
$scope.get = function(search){ // return some data from service };
$rootScope.search = function(data){
$scope.get(data);
};
}]);
Ctrl.spec.js
describe('Ctrl', function(){
beforeEach(module('app'));
var $controller, $scope = {}, $rootScope = {};
beforeEach(inject(function(_$controller_, _$rootScope_){
$controller = _$controller_;
$rootScope = _$rootScope_;
}));
describe('Data', function(){
beforeEach(function() {
$controller('Ctrl', { $scope:$scope, $rootScope:$rootScope});
spyOn($scope, 'get');
});
it('$rootScope.search', function(){
$rootScope.search();
expect($scope.get).toHaveBeenCalled();
});
})
});
Error
TypeError: $scope.get is not a function at Scope.$rootScope.search
Please help.
Actually I have to use the same from view as a global method.
If you want to provide global methods on $rootScope, do it from a .run block instead of a controller.
App.run([$rootScope', function($rootScope){
function get = function(search){ // return some data from service };
$rootScope.search = get;
}]);
But putting common functions in a factory is the recommended approach.
App.factory('mySearch', function(){
function get = function(search){ // return some data from service };
return { search: get };
});
Then inject the custom search in your controllers:
App.controller('Ctrl', ['$scope', 'mySearch', function($scope, mySearch){
var vm = $scope;
vm.data = mySearch.search(data);
}]);

$scope variable being undefined despite being set inside a $watch function

I'm trying to insert some things into my Firebase database using a user's uid but it comes out undefined for some reason. Take a look at the code below:
The main controller that sets the user's data information (authData) when the page loads:
flickrApp.controller('mainCtrl', ['$scope', '$rootScope', '$firebase', 'Auth', 'shared', function($scope, $rootScope, $firebase, Auth, shared) {
Auth.$onAuth(function(authData) {
shared.setAuth(authData);
$scope.authData = shared.getAuth();
});
}]);
The service which handles the authentication state and shares it across my controllers:
flickrApp.service('shared', function() {
var authentication = false;
return {
getAuth: function () {
return authentication;
},
setAuth: function (auth) {
authentication = auth;
}
};
});
Here is where it doesn't work, in my tags controller. The $scope.authData is being set correctly in the $watch function but when I try to use it in the var ref line it says that $scope.authData is undefined (so therefore I can't access the uid). I can't figure out why this isn't working as it should be..
Do I have to use $apply with the watcher function as well or what is wrong?
flickrApp.controller('tagsCtrl', ['$scope', '$rootScope', '$firebase', 'shared', function($scope, $rootScope, $firebase, shared) {
$scope.tagsList = [];
$scope.shared = shared;
$scope.$watch('shared.getAuth()', function(authData) {
$scope.authData = authData;
console.log($scope.authData);
});
var ref = new Firebase ('https://flickr.firebaseio.com/users/' + $scope.authData.uid);
var sync = $firebase(ref);
$scope.addTag = function(tag) {
$scope.tagsList.push(tag);
sync.$set({favoriteTags: $scope.tagsList});
}
}]);
I think the issue is, assignment to ref is being done before the data of $scope.authData is set in $watch. Try to change your code to this:
flickrApp.controller('tagsCtrl', ['$scope', '$rootScope', '$firebase', 'shared', function($scope, $rootScope, $firebase, shared) {
$scope.tagsList = [];
$scope.shared = shared;
var ref,sync;
$scope.$watch('shared.getAuth()', function(authData) {
$scope.authData = authData;
console.log($scope.authData);
if($scope.authData){
ref = new Firebase ('https://flickr.firebaseio.com/users/' + $scope.authData.uid);
sync = $firebase(ref);
}
});
$scope.addTag = function(tag) {
$scope.tagsList.push(tag);
sync.$set({favoriteTags: $scope.tagsList});
}
}]);

Jasmine unit test $scope access

Why am I unable to access my function in my Controller? The code functions like I would expect it too, however, it doesn't seem to want to allow me access to my function that I'm trying to unit test. It should just return a simple bool, but it's getting killed somewhere.
Here's some code:
RTHelper.js
describe('Unit: LocationController', function () {
var $scope, $httpBackend, $location, injector, ctrl, $controller;
//beforeEach(function () {
// angular.module('TDE').controller('LocationController'); //
// inject(function ($injector) {
// $rootScope = $injector.get('$rootScope');
// $scope = $rootScope.$new();
// //ctrl = $injector.get('$controller')("LocationController", { $scope: $scope });
// injector = $injector;
// ctrl = $injector.get('$controller');
// //scope = $injector.get('$rootScope').$new();
// $httpBackend = $injector.get('$httpBackend');
// $location = $injector.get('$location');
// });
//});
//both beforeEach methods work(which one is better? I don't know), so things are getting loaded
beforeEach(function () {
angular.module('TDE');
inject(function ($injector) {
$location = $injector.get('$location');
$rootscope = $injector.get('$rootScope');
$scope = $rootscope.$new();
$controller = $injector.get('$controller');
ctrl = function () {
return $controller('LocationController', {
'$scope': $scope
})
};
})
});
it("should just be a holder for something for later", function () {
expect($scope.BoolCondition()).toBeDefined(); //I don't care what it returns as long as it's accessed honestly
});
})
LocationController.js
angular
.module('TDE')
.controller('LocationController', ['$rootScope', '$scope', '$location', '$window', '$document', 'LocationService', 'HeaderFooterService', 'SearchService', 'TranslationService', 'MTDE_CONFIG', 'LocationPartnerAssignmentService', 'ExperimentService', function ($rootScope, $scope, $location, $window, $document, $LocationService, $HeaderFooterService, $SearchService, $TranslationService, $MTDE_CONFIG, $LocationPartnerAssignmentService, $ExperimentService) {
$scope.BoolCondition = function(myCondition){
if(//blah blah condition test on myCondition)
{
return true
}
else
{
return false;
}
}
How would I go about getting to that BoolCondition? I'm new to this so you can imagine the struggle of writing unit tests after never having done unit testing. I've also gone through countless examples and I've done some generic tests, so I'm not totally un-versed.
You're not bootstrapping the module under test correctly. You should use angular.mock.module inside the test
You're not instantiating the controller (where's the call to ctrl()?)
Here's the complete working example and the fiddle that runs it:
describe('Unit: LocationController', function () {
var $scope, $location, ctrl;
beforeEach(function () {
angular.mock.module('TDE');
inject(function (_$location_, _$rootScope_, _$controller_) {
$location = _$location_;
$scope = _$rootScope_.$new();
ctrl = _$controller_('LocationController', {
'$scope': $scope
})
});
});
it("should just be a holder for something for later", function () {
expect($scope.BoolCondition()).toBeDefined();
expect($scope.BoolCondition()).toBeTruthy();
});
})

Categories