Testing angularjs directive generating html code or not - angularjs

I have a directive something like this which I want to test,
.directive('gridHeader', function() {
return {
restrict: 'A',
replace: true,
scope: false,
compile: function(tEle, tAttrs, transcludeFn) {
var h3 = tEle.find('h3');
var temp = h3.html();
temp = temp.replace('xxxx', tAttrs.gridHeader);
h3.html(temp);
},
template: '<div class="grid-header">' +
'<h3>Showing {{grid.data.records}} xxxx</h3>' +
'<div class="pull-right">' +
'</div>' +
'<div class="clearfix"></div>' +
'</div>'
}
})
I have tried something like this, but its not working
it('directive: should generate all required html elements', function() {
var items = angular.element('div.grid-header');
console.log(items.length); //always returning 0
expect(true).toBe(true);
});

I'm not a fan of checking directive HTML in unit tests (I feel that's better suited in e2e) however something like this should suffice...
Save a reference to the compiled element
var element;
...
inject(function($rootScope, $compile) {
var scope = $rootScope.$new();
scope.grid = { data: { records: 'records' } };
element = $compile('<div grid-header="foo"></div>')(scope);
$rootScope.$digest();
});
Then you can run tests against it
it('whatever', function() {
expect(element.hasClass('grid-header')).toBeTruthy();
expect(element.find('h3').text()).toEqual('Showing records foo');
});

Related

How to make directive use the controller specified in directive attribute?

So I have a directive:
<directive data="user" templateUrl="./user.html" controller="UserController"></directive>
I want that directive to use the controller specified in "controller" attribute, as you see above.
Is it possible with AngularJS directives? Or should I do it other way, maybe with components?
My code currently looks like this:
app.directive('directive', function() {
var controllerName = "UserController"; // i want that to dynamicaly come from attribute
// check if controller extists:
var services = [];
app['_invokeQueue'].forEach(function(value){
services[value[2][0]] = true;
});
if (!services[controllerName]) controllerName = false;
return {
scope: { 'data' : '=' },
link: function (scope) {
Object.assign(scope, scope.data);
},
templateUrl: function(element, attr) {
return attr.templateurl;
},
controller: controllerName
}
});
You can do following (not exactly what you ask - it creates bunch of nested scopes, but should be sufficient):
.directive('directive', () => {
scope: { 'data' : '=' },
template: (elem, attrs) => {
return '<div ng-controller="' + attrs.controller + ' as vm"><div ng-include="' + attrs.template + '"></div></div>';
}
});
<directive data="user" templateUrl="./user.html" controller="UserController"></directive>
you may use $templateCache directly instead of ng-include
if you need controller/template/... to be dynamic, you need to observe/watch + dom manipulation + recompile stuff
Okay, so after analysing Petr's answer I post the working code using nested divs:
app.directive('directive', function() {
return {
scope: { 'data' : '=' },
link: function (scope) {
// this makes your fields available as {{name}} instead of {{user.name}}:
Object.assign(scope, scope.data);
},
template: function(element, attrs) {
var controllerName = attrs.controller;
var controllerString = controllerName + ' as vm';
// check if controller extists:
var services = [];
app['_invokeQueue'].forEach(function(value){
services[value[2][0]] = true;
})
if (!services[controllerName]) {
return '<div ng-include="\'' + attrs.templateurl + '\'"></div>';
} else {
return '<div ng-controller="' + controllerString + '"><div ng-include="\'' + attrs.templateurl + '\'"></div></div>';
}
}
}
});

Mocking out required controllers in directive tests

I am having a hard time trying to figure out how I mock out a required controller for a directive I have written that's the child of another.
First let me share the directives I have:
PARENT
angular
.module('app.components')
.directive('myTable', myTable);
function myTable() {
var myTable = {
restrict: 'E',
transclude: {
actions: 'actionsContainer',
table: 'tableContainer'
},
scope: {
selected: '='
},
templateUrl: 'app/components/table/myTable.html',
controller: controller,
controllerAs: 'vm',
bindToController: true
};
return myTable;
function controller($attrs, $scope, $element) {
var vm = this;
vm.enableMultiSelect = $attrs.multiple === '';
}
}
CHILD
angular
.module('app.components')
.directive('myTableRow', myTableRow);
myTableRow.$inject = ['$compile'];
function myTableRow($compile) {
var myTableRow = {
restrict: 'A',
require: ['myTableRow', '^^myTable'],
scope: {
model: '=myTableRow'
},
controller: controller,
controllerAs: 'vm',
bindToController: true,
link: link
};
return myTableRow;
function link(scope, element, attrs, ctrls) {
var self = ctrls.shift(),
tableCtrl = ctrls.shift();
if(tableCtrl.enableMultiSelect){
element.prepend(createCheckbox());
}
self.isSelected = function () {
if(!tableCtrl.enableMultiSelect) {
return false;
}
return tableCtrl.selected.indexOf(self.model) !== -1;
};
self.select = function () {
tableCtrl.selected.push(self.model);
};
self.deselect = function () {
tableCtrl.selected.splice(tableCtrl.selected.indexOf(self.model), 1);
};
self.toggle = function (event) {
if(event && event.stopPropagation) {
event.stopPropagation();
}
return self.isSelected() ? self.deselect() : self.select();
};
function createCheckbox() {
var checkbox = angular.element('<md-checkbox>').attr({
'aria-label': 'Select Row',
'ng-click': 'vm.toggle($event)',
'ng-checked': 'vm.isSelected()'
});
return angular.element('<td class="md-cell md-checkbox-cell">').append($compile(checkbox)(scope));
}
}
function controller() {
}
}
So as you can probably see, its a table row directive that prepends checkbox cells and when toggled are used for populating an array of selected items bound to the scope of the parent table directive.
When it comes to unit testing the table row directive I have come across solutions where can mock required controllers using the data property on the element.
I have attempted this and am now trying to test the toggle function in my table row directive to check it adds an item to the parent table directive's scope selected property:
describe('myTableRow Directive', function() {
var $compile,
scope,
compiledElement,
tableCtrl = {
enableMultiSelect: true,
selected: []
},
controller;
beforeEach(function() {
module('app.components');
inject(function(_$rootScope_, _$compile_) {
scope = _$rootScope_.$new();
$compile = _$compile_;
});
var element = angular.element('<table><tbody><tr my-table-row="data"><td></td></tr></tbody></table>');
element.data('$myTableController', tableCtrl);
scope.data = {foo: 'bar'};
compiledElement = $compile(element)(scope);
scope.$digest();
controller = compiledElement.controller('myTableRow');
});
describe('select', function(){
it('should work', function(){
controller.toggle();
expect(tableCtrl.selected.length).toEqual(1);
});
});
});
But I'm getting an error:
undefined is not an object (evaluating 'controller.toggle')
If I console log out the value of controller in my test it shows as undefined.
I am no doubt doing something wrong here in my approach, can someone please enlighten me?
Thanks
UPDATE
I have come across these posts already:
Unit testing a directive that defines a controller in AngularJS
How to access controllerAs namespace in unit test with compiled element?
I have tried the following, given I'm using controllerAs syntax:
var element = angular.element('<table><tr act-table-row="data"><td></td></tr></table>');
element.data('$actTableController', tableCtrl);
$scope.data = {foo: 'bar'};
$compile(element)($scope);
$scope.$digest();
console.log(element.controller('vm'));
But the controller is still coming up as undefined in the console log.
UPDATE 2
I have come across this post - isolateScope() returning undefined when testing angular directive
Thought it could help me, so I tried the following instead
console.log(compiledElement.children().scope().vm);
But still it returns as undefined. compiledElement.children().scope() does return a large object with lots of angular $$ prefixed scope related properties and I can see my vm controller I'm trying to get at is buried deep within, but not sure this is the right approach
UPDATE 3
I have come across this article which covers exactly the kind of thing I'm trying to achieve.
When I try to implement this approach in my test, I can get to the element of the child directive, but still I am unable to retrieve it's scope:
beforeEach(function(){
var element = angular.element('<table><tr act-table-row="data"><td></td></tr></table>');
element.data('$actTableController', tableCtrl);
$scope.data = {foo: 'bar'};
compiledElement = $compile(element)($scope);
$scope.$digest();
element = element.find('act-table-row');
console.log(element);
console.log(element.scope()); //returns undefined
});
I just wonder if this is down to me using both a link function and controllerAs syntax?
You were very close with the original code you'd posted. I think you were just using .controller('myTableRow') on the wrong element, as your compiledElement at this point was the whole table element. You needed to get a hold of the actual tr child element in order to get the myTableRow controller out of it.
See below, specifically:
controller = compiledElement.find('tr').controller('myTableRow');
/* Angular App */
(function() {
"use strict";
angular
.module('app.components', [])
.directive('myTableRow', myTableRow);
function myTableRow() {
return {
restrict: 'A',
require: ['myTableRow', '^^myTable'],
scope: {
model: '=myTableRow'
},
controller: controller,
controllerAs: 'vm',
bindToController: true,
link: link
};
function link($scope, $element, $attrs, $ctrls) {
var self = $ctrls.shift(),
tableCtrl = $ctrls.shift();
self.toggle = function() {
// keeping it simple for the unit test...
tableCtrl.selected[0] = self.model;
};
}
function controller() {}
}
})();
/* Unit Test */
(function() {
"use strict";
describe('myTableRow Directive', function() {
var $compile,
$scope,
compiledElement,
tableCtrl = {},
controller;
beforeEach(function() {
module('app.components');
inject(function(_$rootScope_, _$compile_) {
$scope = _$rootScope_.$new();
$compile = _$compile_;
});
tableCtrl.enableMultiSelect = true;
tableCtrl.selected = [];
var element = angular.element('<table><tbody><tr my-table-row="data"><td></td></tr></tbody></table>');
element.data('$myTableController', tableCtrl);
$scope.data = {
foo: 'bar'
};
compiledElement = $compile(element)($scope);
$scope.$digest();
controller = compiledElement.find('tr').controller('myTableRow');
//console.log(controller); // without the above .find('tr'), this is undefined
});
describe('select', function() {
it('should work', function() {
controller.toggle();
expect(tableCtrl.selected.length).toEqual(1);
});
});
});
})();
<link rel="stylesheet" href="//cdn.jsdelivr.net/jasmine/2.0.0/jasmine.css" />
<script src="//cdn.jsdelivr.net/jasmine/2.0.0/jasmine.js"></script>
<script src="//cdn.jsdelivr.net/jasmine/2.0.0/jasmine-html.js"></script>
<script src="//cdn.jsdelivr.net/jasmine/2.0.0/boot.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular-mocks.js"></script>
Here is an example to quote the use of angular directives using the parent child relationship.
The definition of annotated-image looks like this:(which is the parent)
angular.module('annotatedimage').directive('annotatedImage', function() {
function AnnotatedImageController(scope) {}
return {
{
restrict: 'E',
template: [
'<annotated-image-controls annotations="configuration.annotations"></annotated-image-controls>',
'<annotated-image-viewer src="configuration.image" annotations="configuration.annotations"></annotated-image-viewer>',
'<annotated-image-current></annotated-image-current>'
].join('\n'),
controller: ['$scope', AnnotatedImageController],
scope: {
configuration: '='
}
}
};
});
Now for the annotatedImageController , annotatedImageViewer and the annotatedImageCurrent which are the children.
angular.module('annotated-image').directive('annotatedImageControls', function() {
function link(scope, el, attrs, controller) {
scope.showAnnotations = function() {
controller.showAnnotations();
};
controller.onShowAnnotations(function() {
scope.viewing = true;
});
}
return {
restrict: 'E',
require: '^annotatedImage',
template: [
'<div>',
'<span span[data-role="show annotations"] ng-click="showAnnotations()" ng-hide="viewing">Show</span>',
'<span span[data-role="hide annotations"] ng-click="hideAnnotations()" ng-show="viewing">Hide</span>',
'<span ng-click="showAnnotations()">{{ annotations.length }} Annotations</span>',
'</div>'
].join('\n'),
link: link,
scope: {
annotations: '='
}
};
});
angular.module('annotated-image').directive('annotatedImageViewer', function() {
function link(scope, el, attrs, controller) {
var canvas = el.find('canvas');
var viewManager = new AnnotatedImage.ViewManager(canvas[0], scope.src);
controller.onShowAnnotations(function() {
viewManager.showAnnotations(scope.annotations);
});
}
return {
restrict: 'E',
require: '^annotatedImage',
template: '<canvas></canvas>',
link: link,
scope: {
src: '=',
annotations: '='
}
};
});
The same can be done for the annotatedImageCurrent
Summary
<parent-component>
<child-component></child-component>
<another-child-component></another-child-component>
</parent-component>
Parent Component
module.directive('parentComponent', function() {
function ParentComponentController(scope) {
// initialize scope
}
ParentComponentController.prototype.doSomething = function() {
// does nothing here
}
return {
restrict: 'E',
controller: ['$scope', ParentComponentController],
scope: {}
};
});
Child Component
module.directive('childComponent', function() {
function link(scope, element, attrs, controller) {
controller.doSomething();
}
return {
restrict: 'E',
require: '^parentComponent',
link: link,
scope: {}
}
});

How to test function within directive link

I have a directive for which i am trying to write some unit test for:
return {
restrict: 'E',
replace: true,
controllerAs: 'vm',
transclude: true,
template: '<ul>' +
'<li ng-show="vm.hideItem">Home</li>' +
'<li ng-show="vm.hideItem">Products</li>' +
'<li ng-show="!vm.hideItem">Cart</li>' +
'<li ng-show="vm.hideItem">Contact Us</li>' +
'</ul>',
link: function(scope, element, attrs, vm) {
function getData(index) {
if (index){
vm.hideItem = true
}
else {
var li = element.find("li");
li.attr("context-driven", "");
}
}
function getIndex(){
index = myService.getIndex();
getData(index);
}
getIndex();
},
controller: function(){}
};
I have the following, which pass:
describe('<-- myDirective Spec ------>', function () {
var scope, $compile, element, myService;
beforeEach(angular.mock.module('MyApp'));
beforeEach(inject(function (_$rootScope_, _$compile_, _myService_) {
scope = _$rootScope_.$new();
$compile = _$compile_;
myService = _myService_;
var html = '<my-directive></my-directive>';
element = $compile(angular.element(html))(scope);
scope.$digest();
}));
it('should return an unordered list', function () {
var ul = element.find('ul');
expect(ul).toBeDefined();
});
How can i test the call of getIndex, getData and ensure myService has been called?
The key to successful directive testing is moving all view-related logic to controller, i.e.
this.getIndex = function () {
index = myService.getIndex();
getData(index);
}
After the element was compiled in spec, the controller instance can be retrieved and spied with
var ctrl = element.controller('myDirective');
spyOn(ctrl, 'getIndex').and.callThrough();
The good thing about writing specs is that they show design flaws. In current case it is DOM manual operation in getData. It is not clear from the code what context-driven attribute is for, but the same thing has to be achieved in Angular (data binding) and not jQuery (DOM manipulation) fashion in order to be test-friendly.

Updating directive when scope variable changes in ng-grid?

I have a requirement for consuming an array of objects within ng-grid that are custom styled to look like a tag (similar to tags on here).
I have taken the approach of using a cellTemplate and have created a custom directive for this.
What is happening is when you sort, other columns change but the 'Tags' column does not, it stays as is, like the directive isn't getting updated.
Here is my directive:
app.directive('tag', function($compile){
return {
restrict: 'EA',
link: function(scope, element, attrs) {
attrs.$observe('tags', function(value) {
var array = JSON.parse(value);
var newHtml = '<ul>';
for(var i=0;i<array.length;i++)
{
newHtml += '<li>' + array[i].text + '</li>';
}
newHtml += '</ul>';
var e = $compile(newHtml)(scope);
element.replaceWith(e);
});
}
}
});
Here is a plunker: http://plnkr.co/edit/OxeUPaLLWtiCnvmgehnl
Thanks
Don't know if this fulfils all your requirements but you can change your tag directive to this:
app.directive('tag', function($compile){
var ddo = {
restrict: 'EA',
template: '<div><ul><li ng-repeat="tag in tags">{{tag}}</li></div>',
scope: { tags: "=tags" }
};
return ddo;
});
Or if you want to keep your code, just change the DOM first and compile it afterwards:
app.directive('tag', function($compile){
var ddo = {
restrict: 'EA',
scope: { tags: "#tags" },
link: function(scope, element, attrs) {
attrs.$observe('tags', function(value) {
var array = JSON.parse(value);
var newHtml = '<ul>';
for(var i=0;i<array.length;i++)
{
newHtml += '<li>' + array[i].text + '</li>';
}
newHtml += '</ul>';
element.html(newHtml);
$compile(newHtml)(scope);
});
}
};
return ddo;
});
Edit: Also, if all you want to do is change the layout, nothing stops you from calling ng-repeat in your cellTemplate:
cellTemplate: '<ul><li ng-repeat="val in row.entity.arr">{{val}}</li></ul>'}

Unit Testing AngularJS Directive that uses ng-switch

I want to unit test my directive. Here is the code:
.directive('getData', function() {
return {
restrict: 'C',
replace: true,
transclude: true,
scope: { myData:'#myData' },
template: '<div ng-switch="myData">' +
'<div ng-switch-when="4">Real Data</div>' +
'<div ng-switch-when="5">False Data</div>' +
'<div ng-switch-default>No Data</div>' +
'</div>'
}
Here is my attempt for unit testing:
describe('Testing', function() {
var $compile, $rootScope;
beforeEach(module('myApp'));
beforeEach(inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
it('get some data', function() {
var c = $compile("<get-data></get-data>")($rootScope);
$rootScope.$digest();
expect(c.html()).toContain(''); //don't know how to do this part
});
});
I don't know how to test ng-switch. I feel like I am missing a lots of things here. Any help would be greatly appreciated. Here is the plnkr: http://plnkr.co/edit/kgygmzXT3eUMw1iNWnwP?p=preview
Try this:
Directive
app.directive('getData', function () {
return {
restrict: 'E,C', // Added E so <get-data></get-data> works
replace: true,
transclude: true,
scope: {
myData: '#' // Removed #myData since it's not needed here
},
template: '<div ng-switch on="myData">' +
' <div ng-switch-when="4">Real Data</div>' +
' <div ng-switch-when="5">False Data</div>' +
' <div ng-switch-default class="grid">No Data</div>' +
'</div>'
}
});
Tests
describe('get-data test', function() {
var $scope,
$compile;
beforeEach(function() {
module('plunker');
inject(function($rootScope, _$compile_) {
$scope = $rootScope.$new();
$compile = _$compile_;
});
});
it('renders Real Data when my-data is 4', function() {
// Arrange
var element = $compile('<get-data my-data="4"></get-data>')($scope);
// Act
element.scope().$digest();
// Assert
expect(element.find('div').html()).toBe('Real Data');
});
// Other tests omitted for brevity's sake
});
Check out this Plunker to see the other tests.
Notice that in order for the directive to render its markup, a digest cycle must occur in the directive's scope. To do that, you need to use element.scope() to get to that scope and call $digest on it.
Finally, since testing directives is almost all about checking if the DOM is being correctly manipulated I recommend adding jQuery as a dependency of your tests so you can take advantage of its selectors. It wasn't needed here, but it can save you a lot of time as your directive becomes more complex.

Resources