I am relatively new to jasmine tests, and I've got some problem with it. I try to test this directive :
DIRECTIVE
myApp.LoadingsDirective = function() {
return {
restrict: 'E',
replace: true,
template: '<div class="loading"><img src="http://www.nasa.gov/multimedia/videogallery/ajax-loader.gif" width="20" height="20" /></div>',
link: function (scope, element, attrs) {
scope.$watch(
function(scope) {
return scope.$eval(attrs.show);
},
function(val) {
if (val){
$(element).show();
}
else{
$(element).hide();
}
})
}
}
}
myApp.directive('loading', myApp.LoadingsDirective);
This directive just show a loading icon until the result of a asynchronious request replace it.
I try something like this :
TEST
describe('Testing directives', function() {
var $scope, $compile, element;
beforeEach(function() {
module('myApp');
inject(function($rootScope, _$compile_) {
$scope = $rootScope.$new();
$compile = _$compile_;
});
});
it('ensures directive show the loading when show attribut is true', function() {
// GIVEN
var element = $compile('<div><loading show="true"> </loading></div>')($scope);
var loadingScope = element.find('loading').scope();
// WHEN
loadingScope.$watch();
// THEN
expect(loadingScope.show).toBe('true');
});
});
What is the best way to test this type of directive ? How to get access to attributs and test it ?
I always do it this way (coffeescript, but you'll get the idea):
'use strict';
describe 'Directive: yourDirective', ->
beforeEach module('yourApp')
# angular specific stuff
$rootScope = $compile = $scope = undefined
beforeEach inject (_$rootScope_, _$compile_) ->
$rootScope = _$rootScope_
$scope = $rootScope.$new()
$compile = _$compile_
# object specific stuff
element = createElement = undefined
beforeEach inject () ->
createElement = () ->
element = angular.element("<your-directive></your-directive>")
$compile(element)($scope)
$scope.$digest()
it "should have a headline", ->
createElement()
element.find("a").click()
$scope.$apply()
expect(element.find("input").val()).toEqual("foobar")
expect($scope.inputModel).toEqual("foobar")
And this could be the directive:
<your-directive>
<a ng-click="spanModel='foobar'">set inputModel</a>
<input ng-model="inputModel">
</your-directive>
First, I extract the creation of your element into a function. This allows you to do some initial setup before the directive is created.
Then I perform some actions on my directive. If you want to apply this actions into your scope (remember in jasmine you are NOT inside angulars' digest circle), you have to call $scope.$apply() or $scope.$digest() (can't remember right now what the exact difference was).
In the example above, you click on the <a> element, and this has a ng-click attached. This sets the inputModel scope variable.
Not tested, but you'll get the idea
Related
I have a super simple directive that overrides click behavior and does a full page reload on click. I'm having trouble writing a Unit Test for this directive. It looks like the $window doesn't get injected properly as well as this error when running the test:
TypeError: 'undefined' is not an object (evaluating '$event.preventDefault')
reload.directive.js
angular.module('myModule')
.directive('reload', ['$window', function($window) {
return {
restrict: 'A',
scope: {},
transclude: true,
replace: true,
template: '<a ng-click="reload($event)" ng-transclude></a>',
link: function(scope, element, attrs) {
scope.reload = function($event) {
$event.preventDefault();
$window.location.href = attrs.href;
};
}
};
}]);
An example of how I'm using it
<a ui-sref="home", reload>Home Example</a>
Here is my unit test: reload-test.directive.js
describe('Testing reload directive', function() {
beforeEach(module('myModule'));
var window, element, scope;
beforeEach(inject(function($compile, $rootScope, $window) {
scope = $rootScope.$new();
window = $window;
element = $compile('<a reload href="/"></a>')(scope);
scope.$digest();
}));
it('should reload the page with the right url', function() {
var compiledElementScope = element.isolateScope();
compiledElementScope.reload();
expect(window.location.href).toEqual('/');
});
});
UPDATED
Instead of doing any of this, I can just use target="_self" on links which triggers a full page reload.
Your test would be more natural if you will trigger an event.
element.triggerHandler('click');
Then your handler will be called by internal angular mechanisms.
Also your test will be failed when you trying to update window.location, because it causes full page reload. So, you need to mock window here:
var fakeWindow, element, scope;
beforeEach(module('myModule'));
beforeEach(function() {
// define fake instance for $window
module(function($provide) {
fakeWindow = {location: {}};
$provide.value('$window', fakeWindow)
});
});
beforeEach(inject(function($compile, $rootScope) {
scope = $rootScope.$new();
element = $compile('<a reload href="/"></a>')(scope);
scope.$digest();
}));
it('should reload the page with the right url', function() {
var event = jasmine.createSpyObj('clickEvent', ['preventDefault']);
event.type = 'click';
element.triggerHandler(event)
expect(fakeWindow.location.href).toEqual('/');
expect(event.preventDefault).toHaveBeenCalled();
});
Now you can safely test your behaviour without side-effects.
trying to test my directive with jasmine but is not failing where it should because of the wrong date(.demo):
describe("Unit: Testing Directives - ", function() {
var $compile, $rootScope;
beforeEach(module('app'));
beforeEach(inject(function(_$compile_, _$rootScope_){
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
describe("Date Validation Directive - ", function(){
it('should show an date as valid', function(){
$rootScope.demo = '10/01/881';
var templateHTML = angular.element('<input class="blah" type="tel" ng-model="demo" my-date />');
var element = $compile(templateHTML)($rootScope);
$rootScope.$digest();
expect(element.hasClass('ng-valid')).toBe(true);
expect(element.hasClass('ng-invalid')).toBe(false);
});
});
});
this is what my directive looks like:
var app = angular.module('app', []);
app.directive("myDate", function () {
return {
restrict: "A", //only activate on element attribute
require: "ngModel", //get hold of NgModelController
link: function (scope, elm, attrs, ctrl) {
ctrl.$parsers.unshift(function (viewValue) {
var date_regexp = /^(0?[1-9]|[12][0-9]|3[01])[\/\-](0?[1-9]|1[012])[\/\-]\d{4}$/;
if (date_regexp.test(viewValue)) {
// it is valid
ctrl.$setValidity("myDate", true);
return viewValue;
} else {
// it is invalid, return undefined (no model update)
ctrl.$setValidity("myDate", false);
return undefined;
}
});
}
};
});
how can I get it working?
plunkr:http://plnkr.co/edit/GYBvynRqdTTqXxnk7thN?p=preview
This is because of the programmatic assignment of the model value. $parsers run when he value is modified through DOM. When you change the model value programatically it is $formatters that run. So in your directive if you change $parsers to $formatters it will fail the test as you expected.
Plnkr
However you may need both of them in your directive and your real testing should be to test the logic inside the parsers/formatters, not how it is run (It has already been tested by angular) or how the classes are added by angular. If your validations are exposed through say a validator service, or even your directive's controller which provides the validation that would be a good target to test.
I have a directive as below which i want to cover as part of my jasmine unit test but not sure how to get the template value and the values inside the link in my test case. This is the first time i am trying to unit test a directive.
angular.module('newFrame', ['ngResource'])
.directive('newFrame', [
function () {
function onAdd() {
$log.info('Clicked onAdd()');
}
return {
restrict: 'E',
replace: 'true',
transclude: true,
scope: {
filter: '=',
expand: '='
},
template:
'<div class="voice ">' +
'<section class="module">' +
'<h3>All Frames (00:11) - Summary View</h3>' +
'<button class="btn" ng-disabled="isDisabled" ng-hide="isReadOnly" ng-click="onAdd()">Add a frame</button>' +
'</section>' +
'</div>',
link: function (scope) {
scope.isDisabled = false;
scope.isReadOnly = false;
scope.onAdd = onAdd();
}
};
}
]);
Here is an example with explanation:
describe('newFrame', function() {
var $compile,
$rootScope,
$scope,
$log,
getElement;
beforeEach(function() {
// Load module and wire up $log correctly
module('newFrame', function($provide) {
$provide.value('$log', console);
});
// Retrieve needed services
inject(function(_$compile_, _$rootScope_, _$log_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
$log = _$log_;
});
// Function to retrieve a compiled element linked to passed scope
getCompiledElement = function(scope) {
var element = $compile('<new-frame></new-frame>')(scope);
$rootScope.$digest();
return element;
}
// Set up spies
spyOn($log, 'info').and.callThrough();
});
it('test', function() {
// Prepare scope for the specific test
$scope.filter = 'Filter';
$scope.expand = false;
// This will be the compiled element wrapped in jqLite
// To get reference to the DOM element do: element[0]
var element = getCompiledElement($scope);
// Get a reference to the button element wrapped in jqLite
var button = element.find('button');
// Verify button is not hidden by ng-hide
expect(button.hasClass('ng-hide')).toBe(false);
// Verify button is not disabled
expect(button.attr('disabled')).toBeFalsy();
// Click the button and verify that it generated a call to $log.info
button.triggerHandler('click');
expect($log.info).toHaveBeenCalled();
});
});
Demo: http://plnkr.co/edit/tOJ0puOd6awgVvRLmfAD?p=preview
Note that I changed the code for the directive:
Injected the $log service
Changed scope.onAdd = onAdd(); to scope.onAdd = onAdd;
After reading the angular documentation for directives,i was able to solve this. Since the restrict is marked as E, the directive can only be injected through a element name. Earlier i was trying through div like below.
angular.element('<div new-frame></div>')
This will work if restrict is marked as A (attributes). Now i changed my injection in he spec file to match the directive with element name.
angular.element('<new-frame></new-frame>')
Now i was able to get the template and scope attributes in my spec. Just to be sure to accept everything, the combination of A (aatributes), E (elements) and C (class name) can be used in the restrict or any 2 as needed.
I have a directive that I want to unittest, but I'm running into the issue that I can't access my isolated scope. Here's the directive:
<my-directive></my-directive>
And the code behind it:
angular.module('demoApp.directives').directive('myDirective', function($log) {
return {
restrict: 'E',
templateUrl: 'views/directives/my-directive.html',
scope: {},
link: function($scope, iElement, iAttrs) {
$scope.save = function() {
$log.log('Save data');
};
}
};
});
And here's my unittest:
describe('Directive: myDirective', function() {
var $compile, $scope, $log;
beforeEach(function() {
// Load template using a Karma preprocessor (http://tylerhenkel.com/how-to-test-directives-that-use-templateurl/)
module('views/directives/my-directive.html');
module('demoApp.directives');
inject(function(_$compile_, _$rootScope_, _$log_) {
$compile = _$compile_;
$scope = _$rootScope_.$new();
$log = _$log_;
spyOn($log, 'log');
});
});
it('should work', function() {
var el = $compile('<my-directive></my-directive>')($scope);
console.log('Isolated scope:', el.isolateScope());
el.isolateScope().save();
expect($log.log).toHaveBeenCalled();
});
});
But when I print out the isolated scope, it results in undefined. What really confuses me though, if instead of the templateUrl I simply use template in my directive, then everything works: isolateScope() has a completely scope object as its return value and everything is great. Yet, somehow, when using templateUrl it breaks. Is this a bug in ng-mocks or in the Karma preprocessor?
Thanks in advance.
I had the same problem. It seems that when calling $compile(element)($scope) in conjunction with using a templateUrl, the digest cycle isn't automatically started. So, you need to set it off manually:
it('should work', function() {
var el = $compile('<my-directive></my-directive>')($scope);
$scope.$digest(); // Ensure changes are propagated
console.log('Isolated scope:', el.isolateScope());
el.isolateScope().save();
expect($log.log).toHaveBeenCalled();
});
I'm not sure why the $compile function doesn't do this for you, but it must be some idiosyncracy with the way that templateUrl works, as you don't need to make the call to $scope.$digest() if you use an inline template.
With Angularjs 1.3, if you disable debugInfoEnabled in the app config:
$compileProvider.debugInfoEnabled(false);
isolateScope() returns undefined also!
I had to mock and flush the $httpBackend before isolateScope() became defined. Note that $scope.$digest() made no difference.
Directive:
app.directive('deliverableList', function () {
return {
templateUrl: 'app/directives/deliverable-list-directive.tpl.html',
controller: 'deliverableListDirectiveController',
restrict = 'E',
scope = {
deliverables: '=',
label: '#'
}
}
})
test:
it('should be defined', inject(function ($rootScope, $compile, $httpBackend) {
var scope = $rootScope.$new();
$httpBackend.expectGET('app/directives/deliverable-list-directive.tpl.html').respond();
var $element = $compile('<deliverable-list label="test" deliverables="[{id: 123}]"></deliverable-list>')(scope);
$httpBackend.flush();
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
expect($element).toBeDefined();
expect($element.controller).toBeDefined();
scope = $element.isolateScope();
expect(scope).toBeDefined();
expect(scope.label).toEqual('test');
expect(scope.deliverables instanceof Array).toEqual(true);
expect(scope.deliverables.length).toEqual(1);
expect(scope.deliverables[0]).toEqual({id: 123});
}));
I'm using Angular 1.3.
You could configure karma-ng-html2js-preprocessor plugin. It will convert the HTML templates into a javascript string and put it into Angular's $templateCache service.
After set a moduleName in the configuration you can declare the module in your tests and then all your production templates will available without need to mock them with $httpBackend everywhere.
beforeEach(module('partials'));
You can find how to setup the plugin here: http://untangled.io/how-to-unit-test-a-directive-with-templateurl/
In my case, I kept running into this in cases where I was trying to isolate a scope on a directive with no isolate scope property.
function testDirective() {
return {
restrict:'EA',
template:'<span>{{ message }}</span>'
scope:{} // <-- Removing this made an obvious difference
};
}
function testWithoutIsolateScopeDirective() {
return {
restrict:'EA',
template:'<span>{{ message }}</span>'
};
}
describe('tests pass', function(){
var compiledElement, isolatedScope, $scope;
beforeEach(module('test'));
beforeEach(inject(function ($compile, $rootScope){
$scope = $rootScope.$new();
compiledElement = $compile(angular.element('<div test-directive></div>'))($scope);
isolatedScope = compiledElement.isolateScope();
}));
it('element should compile', function () {
expect(compiledElement).toBeDefined();
});
it('scope should isolate', function () {
expect(isolatedScope).toBeDefined();
});
});
describe('last test fails', function(){
var compiledElement, isolatedScope, $scope;
beforeEach(module('test'));
beforeEach(inject(function ($compile, $rootScope){
$scope = $rootScope.$new();
compiledElement = $compile(angular.element('<div test-without-isolate-scope-directive></div>'))($scope);
isolatedScope = compiledElement.isolateScope();
}));
it('element should compile', function () {
expect(compiledElement).toBeDefined();
});
it('scope should isolate', function () {
expect(isolatedScope).toBeDefined();
});
});
I have a directive that uses an isolate scope to pass in data to a directive that changes over time. It watches for changes on that value and does some computation on each change. When I try to unit test the directive, I can not get the watch to trigger (trimmed for brevity, but the basic concept is shown below):
Directive:
angular.module('directives.file', [])
.directive('file', function() {
return {
restrict: 'E',
scope: {
data: '=',
filename: '#',
},
link: function(scope, element, attrs) {
console.log('in link');
var convertToCSV = function(newItem) { ... };
scope.$watch('data', function(newItem) {
console.log('in watch');
var csv_obj = convertToCSV(newItem);
var blob = new Blob([csv_obj], {type:'text/plain'});
var link = window.webkitURL.createObjectURL(blob);
element.html('<a href=' + link + ' download=' + attrs.filename +'>Export to CSV</a>');
}, true);
}
};
});
Test:
describe('Unit: File export', function() {
var scope;
beforeEach(module('directives.file'));
beforeEach(inject(function ($rootScope, $compile) {
scope = $rootScope.$new();
};
it('should create a CSV', function() {
scope.input = someData;
var e = $compile('<file data="input" filename="filename.csv"></file>')(scope);
//I've also tried below but that does not help
scope.$apply(function() { scope.input = {}; });
});
What can I do to trigger the watch so my "In watch" debugging statement is triggered? My "In link" gets triggered when I compile.
For a $watch to get triggered, a digest cycle must occur on the scope it is defined or on its parent. Since your directive creates an isolate scope, it doesn't inherit from the parent scope and thus its watchers won't get processed until you call $apply on the proper scope.
You can access the directive scope by calling scope() on the element returned by the $compile service:
scope.input = someData;
var e = $compile('<file data="input" filename="filename.csv"></file>')(scope);
e.isolateScope().$apply();
This jsFiddler exemplifies that.