Test a call to a passed callback in a directive - angularjs

Im trying to test the logic of calling the callback, passed as parameters to directive.
My directive is initiated as this:
var directive = {
...
scope: {
onSave: '&'
},
controller: 'TmpViewCtrl',
controllerAs: 'vm',
bindToController: true // Isolate scope
};
In the Controller i using storing this as vm variable ( John Pappas styleguide)
function TmpViewCtrl($scope) {
var vm = this;
$scope.save = save;
function save(element) {
if (element !== undefined) {
vm.onSave({element: element});
}
}
In my test im using following beforeEach:
var dataMock = {
onSave: function (elem) {
return true;
},
beforeEach(inject(function (_$compile_, _$rootScope_, _$controller_) {
$compile = _$compile_;
$rootScope = _$rootScope_.$new();
$controller = _$controller_('TmpViewCtrl', {$scope: $rootScope}, dataMock);
// Compile a piece of HTML containing the directive
element = $compile("<div tmp-view ></div>")($rootScope);
// fire all the watches, so the scope expressions will be evaluated
$rootScope.$digest();
}));
My Test looks like
it('If save is triggered, callback should be called', function () {
// Check that the compiled element contains the templated content
var temp = {test: "test"};
spyOn($rootScope, "save");
spyOn($controller, "onSave");
$rootScope.save(temp);
expect($rootScope.save).toHaveBeenCalled();
expect($controller.onSave).toHaveBeenCalled();
});
Well the controller has the last line says that
Expected spy onSave to have been called., but it should be called? Or i am checking false controller scope?

I came here looking for a way to mock callbacks passed to angular 2 components. Found the answer in the page suggested by #silbermm. Basically jasmin.createSpy is what does the trick.
$rootScope.save = jasmin.createSpy("save");
$controller.onSave = jasmin.createSpy("onSave");
expect($rootScope.save).toHaveBeenCalled();
expect($controller.onSave).toHaveBeenCalled();

I was looking for a way to do the same thing. I stumbled across an article describing how to do it - http://busypeoples.github.io/post/testing-components-angular-js/.

Related

How to trigger $onInit or $onChanges implictly in unit testing Angular component controller?

I'm using Angular 1.5.5 and Jasmine as test framework. Currently I have to do something like this so that the test passes:
function createController(bindings) {
return $componentController('myController', null, bindings);
}
beforeEach(inject(function (_$componentController_) {
$componentController = _$componentController_;
}));
describe('on pages updated', function () {
beforeEach(function () {
controller = createController({prop1: 0, prop2: 0});
controller.$onInit(); // you see I have to explitcitly call this $onInit function
});
it('should update isSelected and currentPage', function () {
expect(controller.prop1).toBe(0);
expect(controller.prop2).toBe(0);
controller.prop1= 1;
controller.prop2= 2;
controller.$onChanges(controller); // and $onChanges here as well
expect(controller.prop1).toBe(1);
expect(controller.prop2).toBe(2);
});
});
There is an issue in github regarding this: https://github.com/angular/angular.js/issues/14129
Basically it is working as intended, not calling $onInit or $onChanges automatically.
it makes no sense (or low sense) to execute $onInit, I explain it: $componentController is to instance controllers a kind of replacement for $controller, but instead of creating instances of controllers registered by the controllerProvider it creates instances of controllers registered through directives (the ones that satisfies a component definition). So, once you have the instance of the controller, you can call manually $onInit, and all the lifecycle of your controller, the idea is that you are testing a controller, not a directive (and its relationships).
I don't know if this will help, but for testing components I do the following
beforeEach(module('templates'));
var element;
var scope;
beforeEach(inject(function ($rootScope, $compile) {
scope = $rootScope.$new();
scope.draw = new ol.source.Vector({ wrapX: false });
element = angular.element('<custom-element draw="draw"></custom-element>');
element = $compile(element)(scope);
}));
var controller;
beforeEach(inject(function ($rootScope, $componentController) {
scope = $rootScope.$new();
scope.draw = new ol.source.Vector({ wrapX: false });
controller = $componentController('customElement', {draw: new ol.source.Vector({ wrapX: false })}, { $scope: scope });
}));
and $onInit() and $onChanges() get triggered when they should be, by themselves
You would require to take reference of controller from compiled version of element. As shown below:
describe('Component: Test Method', function () {
beforeEach(inject(function (_$rootScope_) {
scope = _$rootScope_.$new();
}));
it('should set value of selectedPackage to null after $onChanges event', inject(function ($compile) {
// Trigger 1st $onChanges
scope.selectedPackage = 'packageCode';
var element = angular.element('<select-my-tv selected-package="selectedPackage"></select-my-tv>');
element = $compile(element)(scope);
scope.$digest();
//Extract the Controller reference from compiled element
var elementController = element.isolateScope().$ctrl;
// Assert
expect(elementController.selectedPackage).toBeNull();
}));
});
});

How to increase the coverage on testing angular directive?

So here is my angular directive. Simple one that uses a template url
angular.module('my.directives')
.directive('userNameDisplay', function() {
return {
restrict: 'E',
scope: {
user: '=user',
status: '=status'
},
templateUrl: '/partials/userNameDisplay.html'
};
});
The spec is as follows. Again it tries to cover all cases.
describe('user-name-display', function () {
var elm, scope;
beforeEach(module('my.directives', '/partials/userNameDisplay.html'));
beforeEach(inject(function ($compile, $rootScope) {
scope = $rootScope;
elm = angular.element('<user-name-display user="someUser" status="status"></user-name-display>');
$compile(elm)(scope);
}));
it('should have the correct isolate scope values', function () {
scope.someUser = {
name: "John",
color: "blue"
};
scope.status = true;
scope.$digest();
var isoScope = elm.isolateScope();
expect(isoScope.user.name).toBe('John');
expect(isoScope.displayCollaboratorStatus).toBe(true);
});
it('should render html within the partial accordingly', function () {
scope.someUser = {
name: "John"
};
scope.status = false;
scope.$digest();
var cBoxSpan = elm.find("span.user-collaborator-box");
expect(cBoxSpan.length).toBe(0);
var userNameBox = elm.find("span.user-name");
expect(userNameBox[0].innerHTML).toBe("John");
});
});
The coverage report looks like the one below. I am using Karma (which uses Istanbul) to get the code coverage. I am trying to increase it to 100%. I can't figure out from the report what I am missing. It says return statement was never hit, but without it, the isolate bindings will not take place. How can I get the coverage to go 100%?
Here is the image of the report
http://imgur.com/NRzTjyZ
I don't think you'll get coverage from a beforeEach block.
Try adding this test (it's identical to your beforeEach code):
it('should compile', function() {
scope = $rootScope;
elm = angular.element('<user-name-display user="someUser" status="status"></user-name-display>');
$compile(elm)(scope);
});

Jasmine test for scope methods fails

I have an spec that test's if the method in scope was called (see below)
describe("Event Module tests", function () {
var scope, simpleController;
beforeEach(module('SimpleApplication'));
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
simpleController = $controller("SimpleController", {
$scope: scope
});
}));
it("Scope function should be triggered", function () {
spyOn(scope, "trigger");
scope.trigger();//invoke the function on controller
expect(scope.trigger).toHaveBeenCalled();//Passes
expect(scope.isTriggered).toBeTruthy();//Fails
});
});
Application Code(Code to be tested):
angular
.module("SimpleApplication", [])
.controller("SimpleController", function ($scope) {
$scope.message = "Hello World";
$scope.isTriggered = false;
$scope.trigger = function() {
$scope.isTriggered = true;
};
});
Jasmine reports that "Expected false to be truthy.". How come ? since the method sets it to true !!
Update:
For some reason, SpyOn was mutating my object to something it was intended for. So below piece of code works good
it("Scope function should be triggered", function () {
scope.trigger();//invoke the function on controller
expect(scope.isTriggered).toBeTruthy();//Now Passes
});
spyOn doesn't call your method. It just spies. If you want it to be called you have to add something:
spyOn(scope, "trigger").andCallThrough()

AngularJS, Unit testing a directive that uses a service that depends on some resources

I am trying to unit test a directive that is using a service that uses some resources. The issue that I have is when I mock the get method of my resource it will be mocked but the callback function will not be called. Therefore, the result won't be what is expected.
I tried to mock the resources by using spyOn as suggested here, and also $httpBackend.when, but neither worked. When I debug the code it will get to the get method but the get callback function never gets called and therefore, the inner callback myCallback that sets my value never gets called.
I am not sure if my approach is even correct, I appreciate your suggestions.
/ Resource
.factory ('AirportTimeZone', function($resource){
return $resource('/api/airport/:airportId/timezone',{airportId: '#airportId'});
})
/ Service that is using my resources:
angular.module('localizationService', [])
.factory('LocalizationService', ['AirportTimeZone','CurrentLocalization',
function (AirportTimeZone,CurrentLocalization) {
function getAirportTimeZone(airport,myCallback){
var options = {}
var localOptions = AirportTimeZone.get({airportId:airport}, function(data){
options.timeZone = data.timeZoneCode
myCallback(options)
});
}
})
/ Directive
.directive('date',function (LocalizationService) {
return function(scope, element, attrs) {
var airTimeZone
function updateAirportTimeZone(_airportTimeZone){
airTimeZone = _airportTimeZone.timeZone
// call other stuff to do here
}
....
LocalizationService.getAirportTimeZone(airport,updateAirportTimeZone)
....
element.text("something");
}
});
/ Test
describe('Testing date directive', function() {
var $scope, $compile;
var $httpBackend,airportTimeZone,currentLocalization
beforeEach(function (){
module('directives');
module('localizationService');
module('resourcesService');
});
beforeEach(inject(function (_$rootScope_, _$compile_,AirportTimeZone,CurrentLocalization) {
$scope = _$rootScope_;
$compile = _$compile_;
airportTimeZone=AirportTimeZone;
currentLocalization = CurrentLocalization;
// spyOn(airportTimeZone, 'get').andCallThrough();
// spyOn(currentLocalization, 'get').andCallThrough();
}));
beforeEach(inject(function($injector) {
$httpBackend = $injector.get('$httpBackend');
// $httpBackend.when('GET', '/api/timezone').respond({timeZone:'America/New_York',locale:'us-en'});
// $httpBackend.when('GET', '/api/airport/CMH/timezone').respond({timeZone:'America/New_York'});
}))
describe('Date directive', function () {
var compileButton = function (markup, scope) {
var el = $compile(markup)(scope);
scope.$digest();
return el;
};
it('should',function() {
var html = "<span date tz='airport' format='short' airport='CMH' >'2013-09-29T10:40Z'</span>"
var element = compileButton(html,$scope)
$scope.$digest();
expected = "...."
expect(element.html()).toBe(expected);
});
});
})
As commented above:
After setting up responses with $httpBackend.when, you will still need to call $httpBackend.flush() to flush out the mocked response.
References: http://docs.angularjs.org/api/ngMock.$httpBackend - flushing http requests section

Testing element directive - can't access isolated scope methods during tests

I have the following directive.
directivesModule.directive('wikis', function() {
var urlRegex = new RegExp('^(https?)://.+$');
return {
restrict: 'E',
templateUrl: 'templates/wiki-list.html',
scope: {
wikis: '='
},
link: function(scope, element, attrs) {
scope.newWikiURL = '';
scope.$watch('wikis', function() {
if (scope.wikis == undefined || scope.wikis.length === 0) {
scope.class = 'hide';
} else {
scope.class = 'show';
}
}, false);
scope.addWiki = function() {
if (scope.wikis == undefined) {
scope.wikis = [];
}
var nw = scope.newWikiURL;
if (nw != undefined && nw.trim() != '' && urlRegex.exec(nw)) {
scope.wikis.push(nw);
scope.newWikiURL = '';
}
}
}
};
});
When I test it:
describe('Wikis Directive Test Suite', function() {
var scope, elem, directive, linkFn, html;
beforeEach(function() {
html = '<wikis wikis=''></wikis>';
inject(function($compile, $rootScope) {
scope = $rootScope.$new();
scope.wikis = [];
elem = angular.element(html);
$compile(elem)(scope);
scope.$digest();
});
});
it('add Wiki should add a valid wiki URL to artist', function() {
var url = 'http://www.foo.com';
scope.newWikiURL = url;
scope.addWiki();
expect(scope.wikis.length).toBe(1);
expect(scope.wikis[0]).toBe(url);
expect(scope.newWikiURL).toBe('');
});
});
I get an error saying that Object doesn't have an addWiki method. I tried to debug it, and the link function is not called during the test. I suspect that's why the addWiki method is not part of the scope. Anybody knows why I'm getting this error?
By the way, Is it a normal practice to add some logic into the link function of a directive as it would be a Controller itself? Because looking at the code I know that it's why in reality I'm doing.
As per angular 1.2.0 docs, the way to get the isolate scope is through the method isolateScope
scope() - retrieves the scope of the current element or its parent.
isolateScope() - retrieves an isolate scope if one is attached directly to the current element. This getter should be used only on elements that contain a directive which starts a new isolate scope. Calling scope() on this element always returns the original non-isolate scope.
Angular doc - section jQuery/jqLite Extras
BREAKING CHANGE: jqLite#scope()
You need to load the module containing your directive, otherwise angular doesn't know what <wikis> is
Your directive creates an isolate scope, so once it has been compiled you need to get the new scope using elem.isolateScope()
So with those changes:
describe('Wikis Directive Test Suite', function() {
var $scope, scope, elem, directive, linkFn, html;
beforeEach(module('app'));
beforeEach(function() {
html = '<wikis></wikis>';
inject(function($compile, $rootScope, $templateCache) {
$templateCache.put('templates/wiki-list.html', '<div>wiki template</div>');
$scope = $rootScope.$new();
$scope.wikis = [];
elem = angular.element(html);
$compile(elem)($scope);
scope = elem.isolateScope();
scope.$apply();
});
});
it('add Wiki should add a valid wiki URL to artist', function() {
var url = 'http://www.foo.com';
scope.newWikiURL = url;
scope.addWiki();
expect(scope.wikis.length).toBe(1);
expect(scope.wikis[0]).toBe(url);
expect(scope.newWikiURL).toBe('');
});
});
http://jsfiddle.net/QGmCF/1/

Resources