Unit testing AngularJS directive, postLink D3 not changing DOM - angularjs

I'm trying to unit test an AngularJS directive using Jasmine. The directive is meant to add SVG elements to the template in the postLink phase using D3js. This works fine in the actual application.
In the unit test it appears that the D3 code is never executed.
This is a simplified version of the code that still reproduces the error:
angular.module('app', []);
angular.module('app').directive('d3Test', function () {
return {
template: '<div id="map"></div>',
restrict:'E',
scope: {
someAttr: '#',
},
link: function postLink(scope, element, attrs) {
d3.select('#map').append('svg');
}
};
});
This is the unit test:
describe('directive test', function () {
var element, scope;
beforeEach(module('app'));
beforeEach(inject(function ($rootScope, $compile) {
scope = $rootScope.$new();
element = '<d3-test></d3-test>';
element = $compile(element)(scope);
scope.$apply();
}));
it('should have an SVG child element', function () {
expect(element.html()).toEqual('<div id="map"><svg></svg></div>');
});
});
The error I receive is:
PhantomJS 1.9.8 (Mac OS X) directive test should have an SVG child element FAILED
Expected '<div id="map"></div>' to equal '<div id="map"><svg></svg></div>'.
Is my expectation wrong on how I can test for the DOM changes D3 will make?
How can I best test this?

the problem is very simple.
The d3.select method is searching for the element in the window.document.
In your test case your element is in a detached dom element and not part of the window.document.
To fix this you can get the node directly via the directive's element instead of using a global selector.
d3.select(element.find('div#map')[0]).append('svg');
Note: This code works with angular+jQuery. If you don't use jQuery inside of your project you may need to adapt the selector because then your are limited to lookups by tag name.
https://docs.angularjs.org/api/ng/function/angular.element

Related

Angular composite (super) directive not allowing 2 way binding on component (child) directives

I have a need to create a composite directive that incorporates separate fully functional directives. One of my component directives adds an element to the dom and that element binds to a value in the component directive's controller. When the composite directive adds the component directive in the compile function, it seems to work but the piece that has the 2 way binding in the component directive does not appear to get compiled and just renders the {{ctrl.value}} string on the screen. I realize this is a bit convoluted so I have included a plunk to help clarify the issue.
app.directive('compositeDirective', function($compile){
return {
compile: compileFunction
}
function compileFunction(element, attrs){
attrs.$set("component-directive", "");
element.removeAttr("composite-directive");
element.after("<div>Component value when added in composite directive: {{compCtrl.myValue}}</div>");
return { post: function(scope, element){
$compile(element)(scope);
}};
}
});
app.directive('componentDirective', function(){
return {
controller: "componentController as compCtrl",
link: link
};
function link(scope, element){
element.after("<div>Component value: {{compCtrl.myValue}}</div>");
}
});
app.controller('componentController', function(){
var vm = this;
vm.myValue = "Hello";
});
http://plnkr.co/edit/alO83j9Efz62VTKDOVgc
I don't think any compilation will happen as a result of changes in the link function, unless you call $compile manually, i.e.,
app.directive('componentDirective', function($compile){
return {
controller: "componentController as compCtrl",
link: link
};
function link(scope, element){
var elm = $compile("<div>Component value: {{compCtrl.myValue}}</div>")(scope);
element.append(elm);
}
});
Updated plunk: http://plnkr.co/edit/pIixQujs1y6mPMKT4zxK
You can also use a compile function instead of link: http://plnkr.co/edit/fjZMd4FIQ97oHSvetOgU
Also, make sure to use .append() instead of .after().

Jasmine unit test for Angular directive

I have the following angular directive, which adds a tooltip when I hover over a span.
angular.module('mainMod')
.directive('toolTip', [function() {
return {
restrict: 'A',
scope: {
theTooltip: '#toolTip'
},
link: function(scope, element, attrs){
element.tooltip({
delay: 0,
showURL: false,
bodyHandler: function() {
return jQuery('<div class="hover">').text(scope.theTooltip);
}
});
}
}
}])
;
<span ng-show="data.tooltip" class="icon" tool-tip="{{data.tooltip}}"></span>
I'm looking to write a unit test for this directive, atm I can't use jasmine-jquery.
I'm fairly new to writing unit tests, could anyone possibly help me out?
Give me some pointers or point me towards some helpful resources?
Any advice or suggestions would be greatly appreciated.
What I have atm isn't much...
describe('Unit testing tooltip', function() {
var $compile;
var $rootScope;
// Load the myApp module, which contains the directive
beforeEach(module('mainMod'));
// Store references to $rootScope and $compile
// so they are available to all tests in this describe block
beforeEach(inject(function(_$compile_, _$rootScope_){
// The injector unwraps the underscores (_) from around the parameter names when matching
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
it(' ', function() {
// Compile a piece of HTML containing the directive
FAILS HERE --> var element = $compile("<span class='icon' tool-tip='{{data.tooltip}}'></span>")($rootScope);
$rootScope.$digest();
});
});
It's failing with a message of
TypeError: undefined is not a function
I think it's being caused by the ($rootScope) at the end of the line I've specified above.
You have to wrap your DOM content with angular.element first before compiling it. I am not sure what the tooltip module you are using but I used the jQuery UI tooltip instead.
//create a new scope with $rootScope if you want
$scope = $rootScope.$new();
var element = angular.element("<span class='icon' tool-tip='This is the tooltip data'></span>");
//use the current scope has just been created above for the directive
$compile(element)($scope);
One more thing, because you are using isolate scope in your directive, to get the current scope from your directive, you need to call
element.isolateScope()
based on this reference : How to Unit Test Isolated Scope Directive in AngularJS
For a working fiddle, you can found it here : http://jsfiddle.net/themyth92/4w52wsms/1/
Any unit test is basically the same - mock the environment, construct the unit, check that the unit interacts with the environment in the expected manner. In this instance, you'd probably want to mock the tooltip creator
spyOn(jQuery.fn, 'tooltip');
then compile some template using the directive (which you're already doing), then simulate the hover event on the compiled element and then check that the tooltip creator was called in the expected manner
expect(jQuery.fn.tooltip).toHaveBeenCalledWith(jasmine.objectContaining({
// ... (expected properties)
}));
How you simulate the event depends on how the element.tooltip is supposed to work. If it really works the way you're using it in the question code, you don't need to simulate anything at all and just check the expected interaction right after template compilation.

Angular dynamically created directive not executing

Plnkr sample: [http://plnkr.co/edit/jlMQ66eBlzaNSd9ZqJ4m?p=preview][1]
This might not be the proper "Angular" way to accomplish this, but unfortunately I'm working with some 3rd party libraries that I have limited ability to change. I'm trying to dynamically create a angular directive and add it to the page. The process works, at least in the sense where the directive element gets added to the DOM, HOWEVER it is not actually executed - it is just a dumb DOM at this point.
The relevant code is below:
<div ng-app="myModule">
<div dr-test="Static Test Works"></div>
<div id="holder"></div>
<a href="#" onclick="addDirective('Dynamic test works')">Add Directive</a>
</div>
var myModule = angular.module('myModule', []);
myModule.directive('drTest', function () {
console.log("Directive factory was executed");
return {
restrict: 'A',
replace: true,
link: function postLink(scope, element, attrs) {
console.log("Directive was linked");
$(element).html(attrs.drTest);
}
}
});
function addDirective(text){
console.log("Dynamically adding directive");
angular.injector(['ng']).invoke(['$compile', '$rootScope',function(compile, rootScope){
var scope = rootScope.$new();
var result = compile("<div dr-test='"+text+"'></div>")(scope);
scope.$digest();
angular.element(document.getElementById("holder")).append(result);
}]);
}
</script>
While appending the directive to DOM you need to invoke with your module as well in the injector, because the directive drTest is available only under your module, so while creating the injector apart from adding ng add your module as well. And you don't really need to do a scope apply since the element is already compile with the scope. You can also remove the redundant $(element).
angular.injector(['ng', 'myModule']).invoke(['$compile', '$rootScope',function(compile, rootScope){
var scope = rootScope.$new();
var result = compile("<div dr-test='"+text+"'></div>")(scope);
angular.element(document.getElementById("holder")).append(result);
}]);
Demo

Cannot test directive scope with mocha

I have a simple directive with an isolated scope that I'm trying to test. The problem is that I cannot test the scope variables defined in the link function. A watered down sample of my directive and spec below.
Directive:
angular.module('myApp').directive('myDirective', [function(){
return {
scope: {},
restrict: 'AE',
replace: 'true',
templateUrl: 'template.html',
link: function link(scope, element, attrs){
scope.screens = [
'Screen_1.jpg',
'Screen_2.jpg',
'Screen_3.jpg',
];
}
};
}]);
Spec
describe.only('myDirective', function(){
var $scope, $compile;
beforeEach(module('myApp'));
beforeEach(inject(function(_$rootScope_, _$compile_, $httpBackend){
$scope = _$rootScope_.$new();
$compile = _$compile_;
$httpBackend.when('GET', 'template.html').respond();
}));
function create(html) {
var elem, compiledElem;
elem = angular.element(html);
compiledElem = $compile(elem)($scope);
$scope.$digest();
return compiledElem;
};
it('should have an isolated scope', function(){
var element = create('<my-directive></my-directive>');
expect(element.isolateScope()).to.be.exist;
});
});
According to what I've been reading online, this should be working. So after hours of research>fail>repeat I'm leaning to think that it's a bug or a problem with my source code versions. Any ideas?
NOTE I'm testing with Angular 1.2.17, jQuery 1.11.0, Karma ~0.8.3, and latest Mocha
UPDATE 9/19
I updated my directive above, for simplicity's sake I had written down an inline template, but my actual code has an external templateUrl. I also added an $httpBackend.when() to my test to prevent the test from actually trying to get the html file.
I noticed that inlining the template makes everything work fine, but when I use an external template it doesn't fire off the link function.
UPDATE 9/19
I integrated html2js, and now I am able to actually load up the templates from cache and trigger the link function. Unfortunately, isolateScope() is still coming up undefined.
For anyone with the same issue as me, below is the solution.
In MOCHA, if you're using external html templates for you directives, you must use the ng-html2js preprocessor which will cache all your templates in a js file. Once you have this setup, karma will read these instead of trying to fetch the actual html file (this will prevent the UNEXPECTED REQUEST: GET(somepath.html) error).
After this is properly set up. You directive link function or controller scope will be available to your test via isolateScope(). Below is the updated code:
Spec
describe('myDirective', function(){
var $scope, $compile;
beforeEach(module('myApp'));
beforeEach(module('ngMockE2E')); // You need to declare the ngMockE2E module
beforeEach(module('templates')); // This is the moduleName you define in the karma.conf.js for ngHtml2JsPreprocessor
beforeEach(inject(function(_$rootScope_, _$compile_){
$scope = _$rootScope_.$new();
$compile = _$compile_;
}));
function create(html) {
var compiledElem,
elem = angular.element(html);
compiledElem = $compile(elem)($scope);
$scope.$digest();
return compiledElem;
};
it('should have an isolated scope', function(){
var elm = create('<my-directive></my-directive>');
expect(elm.isolateScope()).to.be.defined;
});
});
NOTE I ran into issues where I still got the UNEXPECTED REQUEST: GET... error after I thought I had everything set up correctly, and it turned out that my cached files had a different path than the actual file being requested, look at this post for more help:
Karma 'Unexpected Request' when testing angular directive, even with ng-html2js

Jasmine Unit testing, method does not exist when accessing methods inside a directive

I have the following situation, in my directive I have a method that is declared with "var methodname". I initialise my test but I keep getting an error saying the method does not exist. What am I missing from my test to get around this issue?
//my test
beforeEach(function () {
spyOn(scope, 'innerMethod'); -- fails here with method does not ae
});
//my directive
link: function (scope, elm, attrs) {
var innerMethod = function() {
//do something here
}
});
Edit
I ended up just mocking the functionality inside the jasmine test.
The issue is that innertMethod is not attached to the directive's scope. Instead it only exists in the local scope of the directive.
For your tests to have access to innerMethod you need to attach it to the scope in the directive with scope.innerMethod = function() { ... } and then in your tests you can reference it from the directive, which must be injected.

Resources