I have a angular directive with the following snippet of code, how would I unit test this?
link: function($scope, iElm) {
$(iElm).on('paste', function(){
$timeout(function(){
$scope.lastScan = Date.now();
});
});
}
You need to use $timeout.flush() which will instantly call any $timeout callbacks you have defined, so it essentially makes it into synchronous code for testing purposes.
Try this:
DEMO
app.js
app.directive('myDirective', function($timeout){
return function($scope, iElm) {
// no need to use jQuery
iElm.bind('paste', function(){
console.log('**paste**')
$timeout(function(){
console.log('timeout')
$scope.lastScan = Date.now();
});
});
// $(iElm).on('paste', function(){
// $timeout(function(){
// $scope.lastScan = Date.now();
// });
// });
}
});
appSpec.js
describe("My directive", function() {
var element,
$timeout, $compile, $scope;
// load the module that has your directive
beforeEach(module('plunker'));
beforeEach(inject(function(_$timeout_, _$compile_, _$rootScope_) {
$timeout = _$timeout_;
$compile = _$compile_;
$scope = _$rootScope_.$new();
}));
function compileElement(){
// use $compile service to create an instance of the directive
// that we can test against
element = $compile('<my-directive></my-directive>')($scope);
// manually update scope
$scope.$digest();
}
it("defines a 'lastScan' property", function() {
compileElement();
// trigger paste event
element.triggerHandler('paste');
// angular doesn't know about paste event so need
// to manually update scope
$scope.$digest();
// immediately call $timeout callback
// thus removing any asynchronous awkwardness
$timeout.flush();
// assert that directive has added property to the scope
expect($scope.lastScan).toBeDefined();
});
});
Related
I'm new to jasmine testing. How can I test function call in watch function?
Below is my code. I'm confused about usage of spy in jasmine and how can I handle function call inside watcher.
Do I need to pause fetch() inside watch. Please suggest how to improve my testing skills.
var app = angular.module('instantsearch',[]);
app.controller('instantSearchCtrl',function($scope,$http,$sce){
$scope.$sce=$sce;
$scope.$watch('search', function() {
fetch();
});
$scope.search = "How create an array";
var result = {};
function fetch() {
$http.get("https://api.stackexchange.com/2.2/search?page=1&pagesize=10&order=desc&sort=activity&intitle="+$scope.search+"&site=stackoverflow&filter=!4*Zo7ZC5C2H6BJxWq&key=DIoPmtUvEkXKjWdZB*d1nw((")
.then(function(response) {
$scope.items = response.data.items;
$scope.answers={};
angular.forEach($scope.items, function(value, key) {
var ques = value.question_id;
$http.get("https://api.stackexchange.com/2.2/questions/"+value.question_id+"/answers?page=1&pagesize=10&order=desc&sort=activity&intitle="+$scope.search+"&site=stackoverflow&filter=!9YdnSMKKT&key=DIoPmtUvEkXKjWdZB*d1nw((").then(function(response2) {
$scope.answers[ques]=response2.data.items;
//console.log(JSON.stringify($scope.answers));
});
});
});
}
});
my test case:
describe('instantSearchCtrl', function() {
beforeEach(module('instantsearch'));
var $scope, ctrl;
beforeEach( inject(function($rootScope, $controller) {
// create a scope object for us to use.
$scope = $rootScope.$new();
ctrl = $controller('instantSearchCtrl', {
$scope: $scope
});
}));
/*var $scope = {};
var controller = $controller('instantSearchCtrl', { $scope: $scope });
expect($scope.search).toEqual('How create an array');
//expect($scope.strength).toEqual('strong');*/
it('should update baz when bar is changed', function (){
//$apply the change to trigger the $watch.
$scope.$apply();
//fetch().toHaveBeenCalled();
fetch();
it(" http ", function(){
//scope = $rootScope.$new();
var httpBackend;
httpBackend = $httpBackend;
httpBackend.when("GET", "https://api.stackexchange.com/2.2/search?page=1&pagesize=10&order=desc&sort=activity&intitle="+$scope.search+"&site=stackoverflow&filter=!4*Zo7ZC5C2H6BJxWq&key=DIoPmtUvEkXKjWdZB*d1nw((").respond([{}, {}, {}]);
});
});
});
First you should trigger the watch. For that you should change search value and after that manually run: $scope.$digest() or $scope.$apply()
In order to fully test the fetch function you should also mock the response to the second request (or the mock for all the second level requests if you want to test the iteration).
After that you should add some expect statements. For the controller code they should be:
expect($scope.items).toEqual(mockResponseToFirstRequest);
expect($scope.answers).toEqual(mockResponseCombinedForSecondLevelRequests);
As for using the spy in karma-jasmine tests, those limit the amount of the code tested. A plausible use for spy in this case is to replace the httpBackend.when with spyOn($http, 'get').and.callFake(function () {})
Here is the documentation for using spies https://jasmine.github.io/2.0/introduction.html#section-Spies
I have a controller that looks like:
myApp.controller('myController', function($scope, myService){
$scope.myFunction = function(){
myService.myAJAXCall().then(function(data){
$scope.myVariable = data;
}, function(){
console.log('failed call');
});
};
$scope.myFunction();
$scope.$watch('myVariable', function(newValue){
if(newValue === 'something'){
$scope.test = true;
}
else{
$scope.test = false;
}
});
});
$scope.myFunction() makes a AJAX call to an endpoint and sets $scope.myVariable with the value returned. I call $scope.myFunction() on page load.
I'd like to test the watch on $scope.myVariable. I can do this successfully if I don't call $scope.myFunction() on controller load with:
describe('myTest', function(){
it('should test $scope.test', function(){
scope.test = false;
scope.myVariable = 'something';
scope.$apply();
expect(scope.test).toBeTruthy();
});
});
But this doesn't work if I call $scope.myFunction() on page load. The scope.myVariable = 'something'; declaration is always overwritten by the AJAX call made with myService.myAJAXCall().
How should I write my test so that it doesn't run into this conflict if I call $scope.myFunction() on controller load?
The key was to manually run a digest cycle during test suite set-up using scope.$apply(). This flushed out my AJAX call, which allowed me to test the $watch.
beforeEach(inject(function($controller, $rootScope){
scope = $rootScope.$new();
$controller('myController', {
$scope: scope,
myService: myService
});
// Apply the digest cycle manually at initialization of test suite
scope.$apply();
}));
I have a simple controller with one method.
I set one watcher in the controller and I want to be sure that watcher listener invoked when a model is updated.
Do you know how to check whether watch listener is called?
var app = angular.module('app', []);
app.controller('MainController', ['$scope', function($scope){
$scope.test = 0;
this.method = function(){
console.log('called');
$scope.test = $scope.name.length;
};
$scope.$watch('name', this.method);
}]);
describe('controller method ', function(){
var scope;
var controller;
beforeEach(module('app'));
beforeEach(inject(function($injector){
scope = $injector.get('$rootScope').$new();
var $controller = $injector.get('$controller');
scope.name = 'blabla';
controller = $controller('MainController', { $scope: scope });
}));
it('should watch', function(){
var s = spyOn(controller, 'method');
scope.$digest();
expect(s).toHaveBeenCalled();
});
it('true should be true', function(){
expect(1).toBe(1);
});
});
http://plnkr.co/edit/B7pgd9x3bPtOlY40NkRm?p=preview
You donĀ“t want to spy on the controllerĀ“s method. You might want to rename that method, split it into multiple methods or refactor them in any other way in the future. this will break your test, while the actual behaviour of your controller might not have changed. so focus on testing the controllers behaviour, instead of spying on private methods.
it('should set the testing variable', function(){
// act
scope.name = 'name';
scope.$digest();
// assert
scope.testing.should.eql(4);
});
How do I check if $emit has been called in an unit test of a directive?
My directive is fairly simple (for the purpose of illustration):
angular.module('plunker', [])
.directive('uiList', [function () {
return {
scope: {
lengthModel: '=uiList',
},
link: function (scope, elm, attrs) {
scope.$watch('lengthModel', function (newVal) {
scope.$emit('yolo');
});
}
}
}])
so every time the attribute uiList changes, it will emit the event.
And I have unit test code as follows:
describe('Testing $emit in directive', function() {
var scope;
var element;
//you need to indicate your module in a test
beforeEach(function () {
module('plunker');
inject(function ($rootScope, $compile) {
scope = $rootScope.$new();
scope.row= 1;
spyOn(scope,'$emit');
element = angular.element('<ul id="rows" ui-list="row">');
$compile(element)(scope);
});
});
it('should emit', function() {
scope.$digest();
scope.row = 2;
scope.$digest();
expect(scope.$emit).toHaveBeenCalledWith("yolo");
});
});
This would always give me error stating that the scope.$emit has never been called.
Is there something wrong with the scope? Can someone please help?
Plunker:http://plnkr.co/edit/AqhPwp?p=preview
Your directive creates an isolated scope, which is calling $emit,so you need to spy on this one ;)
describe('Testing $emit in directive', function() {
var scope;
var element;
var elementScope;
beforeEach(module('plunker'));
beforeEach(inject(function($rootScope, $compile) {
scope = $rootScope.$new();
scope.row = 1;
element = angular.element('<ul id="rows" ui-list="row">');
$compile(element)(scope);
scope.$digest();
elementScope = element.scope();
}));
it('should emit', function() {
// arrange
spyOn(elementScope, '$emit');
// act
scope.$apply(function() {
scope.row = 2;
});
// assert
expect(elementScope.$emit).toHaveBeenCalledWith("yolo");
});
});
Fixed plunker here :)
I'm not sure how did it work for glepetre (not sure that it does actually). For me it didn't. The thing is that the directive runs $new on incomming scope so the scope it creates inside and element.scope() are some thing different. You can check their $id fields. So it does not help to spy on the scope you got from element.scope(). I had smilar problem and had to do a little trick to make it work.
Here comes my test :
beforeEach(inject(function () {
scope = $rootScope.$new();
scope.$apply(function () {
scope.field = null;
scope.name = 'nolength';
});
spyOn(scope, '$new').and.returnValue(scope);
spyOn(scope, '$emit');
var element = angular.element('<my-directive name="name" field="field" title="\'Title\'" ></my-directive>');
noLengthValidation = $compile(element)(scope);
scope.$apply();
elementScope = element.scope();
}));
So the trick was to mock $new function on scope so instead of creating a new scope behind the scene and mess the things up it will return itself! Here comes the test itself :
it('The directive should not react on string length if no max-chars attribute presented', function () {
scope.$apply();
noLengthValidation.isolateScope().field = 'fieldisssssssveeeeerryyyyyylooooooonnnngggggggggggggggggggggggggggg';
scope.$apply();
expect(scope.$emit).toHaveBeenCalledWith('ON_MORE_AWASOME_EVENT', 'nolength');
});
And the the part on the directive that calls $emit. Just in case :
function validate() {
scope.validate = 1;
if (scope.field) {
if (!isValid()) {
scope.$emit('ON_AWASOME_EVENT', {code : scope.name, text : 'Field ' + scope.title + ' is feels bad'});
} else {
scope.$emit('ON_MORE_AWASOME_EVENT', scope.name);
}
}
}
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