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.
Related
I am trying to unit test a directive with a two-way bound property (=). The directive works in my app, but I can't get a unit test working that tests the two-way binding.
I have been trying to get this working for days. I've read MANY examples that use some but not all of the features I want to use: controllerAs, bindToController & isolateScope(). (Forget about templateURL, which I also need. I will add that if I can get this working! :)
I'm hoping someone can tell me how to show a change in the parent scope reflected in the isolate scope.
Here is a plunkr that contains the code below:
http://plnkr.co/edit/JQl9fB5kTt1CPtZymwhI
Here is my test app:
var app = angular.module('myApp', []);
angular.module('myApp').controller('greetingController', greetingController);
greetingController.$inject = ['$scope'];
function greetingController($scope) {
// this controller intentionally left blank (for testing purposes)
}
angular.module('myApp').directive('greetingDirective',
function () {
return {
scope: {testprop: '='},
restrict: 'E',
template: '<div>Greetings!</div>',
controller: 'greetingController',
controllerAs: 'greetingController',
bindToController: true
};
}
);
And here is the spec:
describe('greetingController', function () {
var ctrl, scope, rootScope, controller, data, template,
compile, isolatedScope, element;
beforeEach(module('myApp'));
beforeEach(inject(function ($injector) {
rootScope = $injector.get('$rootScope');
scope = rootScope.$new();
controller = $injector.get('$controller');
compile = $injector.get('$compile');
data = {
testprop: 1
};
ctrl = controller('greetingController', {$scope: scope}, data);
element = angular.element('<greeting-directive testprop="testprop"></greeting-directive>');
template = compile(element)(scope);
scope.$digest();
isolatedScope = element.isolateScope();
}));
// PASSES
it('testprop inital value should be 1', function () {
expect(ctrl.testprop).toBe(1);
});
// FAILS: why doesn't changing this isolateScope value
// also change the controller value for this two-way bound property?
it('testprop changed value should be 2', function () {
isolatedScope.testprop = 2;
expect(ctrl.testprop).toBe(2);
});
});
You have to correct the way you're testing your directive. You're directly changing isolatedScope of an object and thereafter verifying the ctrl object which unrelated DOM which you had compiled.
Basically what you should be doing is as soon as you compiled a DOM with scope (here it is <greeting-directive testprop="testprop"></greeting-directive>). So that scope will hold the context of compiled do. In short you can play testprop property value. or same thing will be available inside element.scope(). As soon as you change any value in scope/currentScope. You can see the value gets updated inside directive isolatedScope. One more thing I'd like to mention is when you do controllerAs with bindToController: true, angular adds property with controller alias inside scope that's we verified isolatedScope.greetingController.testprop inside assert
Code
describe('greetingController', function() {
var ctrl, scope, rootScope, controller, data, template,
compile, isolatedScope, currentScope, element;
beforeEach(module('myApp'));
beforeEach(inject(function($injector) {
rootScope = $injector.get('$rootScope');
scope = rootScope.$new();
controller = $injector.get('$controller');
compile = $injector.get('$compile');
data = { testprop: 1 };
ctrl = controller('greetingController', { $scope: scope }, data);
element = angular.element('<greeting-directive testprop="testprop"></greeting-directive>');
template = compile(element)(scope);
scope.$digest();
currentScope = element.scope();
//OR
//currentScope = scope; //both are same
isolatedScope = element.isolateScope();
}));
// First test passes -- commented
it('testprop changed value should be 2', function() {
currentScope.testprop = 2; //change current element (outer) scope value
currentScope.$digest(); //running digest cycle to make binding effects
//assert
expect(isolatedScope.greetingController.testprop).toBe(2);
});
});
Demo Plunker
I am having some trouble reaching my return statement for my angular test. I am using jasmine with karma, and bard js. I used the ng2html preprocesser to $templateCache
templatesCached is the module name given to the files in my templates folder. ha.module.core Houses the directive that I want to test. I am reaching it when I run my debugging tools.
Below is my test. They pass, but the issue I am having is that rootScope does not hold any particular values. Also
element.html() // returns "" in the console. I was expecting my directive back. Is this wrong?
After I run through controller = element.scope I am getting
html = [div.ng-scope]
describe(" directive", function () {
var element,
template,
controller;
beforeEach(function () {
bard.appModule("templatesCached", "ha.module.core");
bard.inject(
"$compile",
"$controller",
"$rootScope",
"haConfig"
);
});
beforeEach(function () {
var html = angular.element("<div explore-hero></div>");
spyOn(myService, "getTemplateUrl");
//console.log("html ", html);
$rootScope = $rootScope.$new();
element = $compile(html)($rootScope);
$rootScope.$digest(element);
controller = element.scope();
element.controller('heroController');
Element.controller is an anonymous function.
console.log('element', element);
});
it("should have a div element", function () {
var result = element[0].querySelectorAll(".container");
expect(element.length).toBe(1); // not a ideal test for creation
expect(result).toBeDefined();
});
});
Here is my template cached.
module.run(['$templateCache', function($templateCache) {
$templateCache.put('/templates/explore-hero-base-template.html',
'<div class="container">\n' +
' <div share-widget></div>\n' +
'///html divs and info'
'</div><!-- .container -->');
}]);
My directive
angular.module('ha.module.core').directive('exploreHero', function(myService) {
var HeroController = function($scope) {
$scope.emit('methods')
};
return {
restrict: 'A',
scope: true,
templateUrl: myService.getTemplateUrl('explore-hero.html),
controller: 'HeroController
}
)
Any insight would help
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 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
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.