angularjs: test service call inside scope.$on - angularjs

I have a directive as below:
app.directive('Directive', ['MyService', '$state',
function (MyService, $state) {
return {
restrict: 'E',
scope: {},
replace: true,
templateUrl: 'partials/sample.html',
link: function (scope, element, attrs) {
scope.$on('onEvent', function (event, result) {
MyService.getInfo($state.params.param1, result.data.value1, result.data.value2)
.then(function (response) {
scope.varX = response.data;
});
});
}
};
}]);
The test case written for this directive is as below:
it('MyDirective test case', function () {
mockBackend.whenGET('partials/sample.html').respond('<div>Test Template</div>');
var scope = rootScope.$new(),
element,
expectedResult = [
{
"data": "dummy"
}];
element = compile('<MyDirective></MyDirective>')(scope);
scope.$digest();
expect(scope.$broadcast).toHaveBeenCalledWith('onEvent', [{value1: 'val1', val2: 'val2'}]);
mockBackend.flush();
var testScope = element.isolateScope();
expect(testScope.varX).toEqual(expectedResult);
});
On running the test case, I get the below error:
Directive: appDirectives MyDirective test case FAILED
Expected undefined to equal [ { data : 'dummy' } ].
The testScope is not getting the var variable assigned.
What am I doing wrong in the test case?

Related

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: {}
}
});

Unit Testing a watch method call in Directive link function

I have the following Directive
.directive("myContainer", function (myContainerService,$window) {
return {
restrict: 'A',
templateUrl: "myContainer/MyContainer.tpl.html",
controller: 'myCtrl',
controllerAs: 'myCtrl',
scope: {
items: '='
},
link: function (scope, element) {
var timeout;
scope.$watch('myCtrl.items', function (items) {
var windowWidth = $window.innerWidth - 65;
window.clearTimeout(timeout);
timeout = window.setTimeout(function () {
myContainerService.saveItems(items);
}, 1000);
}, true);
}
};
})
And here is the Unit Test i have.
describe("myCtrl", function(){
var myCtrl;
var dirEle ;
var myScope;
// var to store the Mock Items
var myContainerService = $injector.get('myContainerService');
var items = [..]
beforeEach(inject(function($compile, $httpBackend){
$httpBackend.whenGET(/.*my-app\/restful-services\/items*./).respond({...});
scope.items = myContainerService.getItems();
dirEle = $compile('<div my-container items="items"></div>')(scope);
scope.$digest();
myScope = dirEle.isolateScope();
myCtrl = myScope.myCtrl;
}));
fit("Saving Items", inject(function($timeout){
spyOn(myContainerService, 'saveItems');
//$timeout.flush();
myScope.$digest();
$timeout.flush();
expect(myContainerService.saveItems).toHaveBeenCalledWith(myCtrl.items);
}));
});
And my test is failing as the saveItems is not getting called at all. Not sure what i am doing wrong.
Appreciate any inputs.
Thanks
You need to be using angulars $timeout that way in your test your $timeout.flush() will work:
.directive("myContainer", function (myContainerService,$window, $timeout) {
return {
restrict: 'A',
templateUrl: "myContainer/MyContainer.tpl.html",
controller: 'myCtrl',
controllerAs: 'myCtrl',
scope: {
items: '='
},
link: function (scope, element) {
var timeout;
scope.$watch('myCtrl.items', function (items) {
var windowWidth = $window.innerWidth - 65;
$timeout.cancel(timeout);
timeout = $timeout(function () {
myContainerService.saveItems(items);
}, 1000);
}, true);
}
};
})

Unit test Angular directive that accesses external element

I have a custom directive that uses an attribute to specify another control that it modifies.
Directive definition object:
{
restrict: 'E',
templateUrl: 'myTemplate.html',
scope: {
targetId: '#'
},
controller: MyController,
controllerAs: 'vm',
bindToController: true
}
A function on the directive's controller modifies the contents of the target element (an input field):
function onSelection (value) {
var $element = $('#' + vm.targetId);
$element.val('calculated stuff');
$element.trigger('input');
}
The unit tests (Jasmine/Karma/PhantomJS) currently append the element to the page. This works, but it seems like a code smell.
beforeEach(inject(function($rootScope, $compile) {
var elementHtml = '<my-directive target-id="bar"></my-directive>' +
'<input type="text" id="bar">';
scope = $rootScope.$new();
angularElement = angular.element(elementHtml);
angularElement.appendTo(document.body); // HELP ME KILL THIS!
element = $compile(angularElement)(scope);
scope.$digest();
}));
afterEach(function () {
angularElement.remove(); // HELP ME KILL THIS!
});
I've tried rewriting the controller function to avoid jQuery; this did not help.
How can I revise the directive or the tests to eliminate the appendTo/remove?
Your best bet is to migrate the directive to an attribute instead of an element. This removes the need for the target-id attribute and you don't need to hunt for the target element.
See http://jsfiddle.net/morloch/621rp33L/
Directive
angular.module('testApp', [])
.directive('myDirective', function() {
var targetElement;
function MyController() {
var vm = this;
vm.onSelection = function() {
targetElement.val('calculated stuff');
targetElement.trigger('input');
}
}
return {
template: '<div></div>',
restrict: 'A',
scope: {
targetId: '#'
},
link: function postLink(scope, element, attrs) {
targetElement = element;
},
controller: MyController,
controllerAs: 'vm',
bindToController: true
};
});
Test
describe('Directive: myDirective', function() {
// load the directive's module
beforeEach(module('testApp'));
var element, controller, scope;
beforeEach(inject(function($rootScope, $compile) {
scope = $rootScope.$new();
element = angular.element('<input my-directive type="text" id="bar">');
$compile(element)(scope);
scope.$digest();
controller = element.controller('myDirective');
}));
it('should have an empty val', inject(function() {
expect(element.val()).toBe('');
}));
it('should have a calculated val after select', inject(function() {
controller.onSelection();
expect(element.val()).toBe('calculated stuff');
}));
});
Here's another suggestion that keeps your logic almost exactly the same: use a second directive to make the target element available on the controller, which you can then pass to your primary directive for processing: http://jsfiddle.net/morloch/p8r2Lz1L/
getElement
.directive('getElement', function() {
return {
restrict: 'A',
scope: {
getElement: '='
},
link: function postLink(scope, element, attrs) {
scope.getElement = element;
}
};
})
myDirective
.directive('myDirective', function() {
function MyController() {
var vm = this;
vm.onSelection = function() {
vm.targetElement.val('calculated stuff');
vm.targetElement.trigger('input');
}
}
return {
template: '<div></div>',
restrict: 'E',
scope: {
targetElement: '='
},
controller: MyController,
controllerAs: 'vm',
bindToController: true
};
})
Test
describe('Directive: myDirective', function() {
// load the directive's module
beforeEach(module('testApp'));
var element, controller, scope;
beforeEach(inject(function($rootScope, $compile) {
scope = $rootScope.$new();
element = angular.element('<input get-element="elementBar" type="text" id="bar"><my-directive target-element="elementBar"></my-directive>');
$compile(element)(scope);
scope.$digest();
controller = $(element[1]).controller('myDirective');
}));
it('should have an empty val', inject(function() {
expect($(element[0]).val()).toBe('');
}));
it('should have a calculated val after select', inject(function() {
controller.onSelection();
expect($(element[0]).val()).toBe('calculated stuff');
}));
});

Testing angular directive scope method

So I can't seem to call a method in my test that is written on the internalScope of an angular directive.
Here is my test
describe('auto complete directive', function () {
var el, $scope, scope;
beforeEach(module('italic'));
beforeEach(module('stateMock'));
beforeEach(module('allTemplates'));
beforeEach(inject(function ($compile, $rootScope, UserService) {
spyOn(UserService, 'getCurrentUser').and.returnValue({});
$scope = $rootScope;
el = angular.element('<auto-complete collection="" input-value="" enter-event="" focus-on="" />');
$compile(el)($scope);
scope = el.isolateScope();
console.log(scope);
$scope.$apply();
}));
it('should', function () {
scope.matchSelected();
expect(scope.showPopup).toBe(false);
});
});
and my directive:
italic.directive('autoComplete', ['$timeout', function($timeout) {
return {
restrict: "E",
template: '',
scope: {
collection: '=',
inputValue: '=',
enterEvent: '=',
focusOn: '='
},
link: function(scope, element) {
scope.matchSelected = function (match) {
scope.inputValue = match;
scope.showPopup = false;
};
}
};
}]);
and the error:
undefined is not a function (called on scope.matchSelected in the test)
I believe that it is rooted in the fact that scope = el.isolateScope(); returns undefined.
It looks like the issue must be to do with two missing braces in the directive. Intead of }]); at the end it should be }}}]);. I'd recommend to take more care when indenting and using braces. If you use indents correctly it will minimise issues such as this. If you were indenting correctly the directive would look like:
italic.directive('autoComplete', ['$timeout', function($timeout) {
return {
restrict: "E",
template: '',
scope: {
collection: '=',
inputValue: '=',
enterEvent: '=',
focusOn: '='
},
link: function(scope, element) {
scope.matchSelected = function (match) {
scope.inputValue = match;
scope.showPopup = false;
};
}
};
}]);
It's best to create your directive in the actual it and not in before, that way you can control the scope properties set on the directive.
describe('auto complete directive', function () {
var $rootScope, $compile;
beforeEach(module('italic'));
beforeEach(inject(function (_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
it('should', function () {
//Arrange
var element = $compile("<auto-complete collection=\"\" input-value=\"\" enter-event=\"\" focus-on=\"\" />")($rootScope);
var scope = element.isolateScope();
var match = "match";
//Act
$rootScope.$digest();
scope.matchSelected(match);
//Assert
expect(scope.showPopup).toBe(false);
});
});
Plunkr

How can I call link directive function after controller code has been run

I'm trying to render code in the link function after the controller resolves the http call but the link function is called before that. How can I call link after $scope.menuHtml has been set?
HTML:
<div id="testD" nav-menu-output="parseMenuJsonAndOutPutMenu()"></div>
DIRECTIVE:
return {
restrict: 'A',
controller: ['$scope', '$q','$http', function ($scope, $q,$http) {
$http.get('ajax/menu' ).then(function (data) {
$scope.menuHtml = generateHtmlMenu(data);
});
}],
link: function(scope, element, attr) {
var templateString = scope.menuHtml;
var compiledTemplate = $compile(templateString)(scope);
compiledTemplate.appendTo("#testD");
}
}
I would suggest using scope.$watch() and rerunning your compiled template code on that. This way you can make as many requests to the menu endpoint as you want and your template will be recompiled.
Here's more information about watch:
http://docs.angularjs.org/api/ng/type/$rootScope.Scope#$watch
Here's an updated version which should work properly:
return {
restrict: 'A',
controller: ['$scope', '$q','$http', function ($scope, $q,$http) {
$http.get('ajax/menu' ).then(function (data) {
$scope.menuHtml = generateHtmlMenu(data);
});
}],
link: function(scope, element, attr) {
scope.$watch('menuHtml', function() {
var templateString = scope.menuHtml;
var compiledTemplate = $compile(templateString)(scope);
compiledTemplate.appendTo("#testD");
});
}
}
You can do it by using async: false
Please Try this code instead of your code,
return {
restrict: 'A',
controller: ['$scope', '$q','$http', function ($scope, $q,$http) {
$http({
method: 'GET',
url: 'ajax/menu',
async: false
}).success(function (data) {
$scope.menuHtml = generateHtmlMenu(data);
})}],
link: function(scope, element, attr) {
var templateString = scope.menuHtml;
var compiledTemplate = $compile(templateString)(scope);
compiledTemplate.appendTo("#testD");
}
}

Resources