Testing Angular Directive with Dependencies like $location and $routeParams - angularjs

Thie question is somewhat related to How do I inject a mock dependency into an angular directive with Jasmine on Karma. But I cant figure it out. Heres the thing:
I have a simple angular directive for rendering a head-part of my apllication with several parameters. One is passed, two came from the URL vie $location and $routeParam. The directive looks like this:
'use strict';
myApp.directive('appHeader', ['$routeParams', '$location', function ($routeParams, $location) {
return {
restrict: 'E',
templateUrl: 'path/to/partials/template.html',
scope: {
icon: '#icon'
},
link: function (scope, element, attributes) {
var lastUrlPart = $location.path().split('/').pop();
scope.project = $routeParams.itemName;
scope.context = lastUrlPart === scope.project ? '' : lastUrlPart;
}
};
}]);
This is called via <app-header icon="bullhorn"></app-header>.
Now I want to add some tests. As for the template rendering I'm done. The following works like expected. The test passes.
describe('appHeader', function () {
var element, scope;
beforeEach(module('myApp'));
beforeEach(module('myAppPartials'));
beforeEach(inject(function ($rootScope, $compile) {
element = angular.element('<app-header icon="foo"></app-header>');
scope = $rootScope;
$compile(element)(scope);
scope.$digest();
}));
it('should contain the glyphicon passed to the directive', function () {
expect(element.find('h1').find('.glyphicon').hasClass('glyphicon-foo')).toBeTruthy();
});
});
Now I want to test that scope.context and scope.project are set accordingly to the dependencies $location and $routeParams, which I want to mock of course. How can I acieve this.
I tried for instance the answer from the question linked above:
beforeEach(module(function ($provide) {
$provide.provider('$routeParams', function () {
this.$get = function () {
return {
itemName: 'foo'
};
};
});
}));
But In my test
it('should set scope.project to itemName from $routeParams', function () {
expect(scope.project).toEqual('foo');
});
scope.project is undefined:
Running "karma:unit:run" (karma) task
Chrome 35.0.1916 (Mac OS X 10.9.3) appHeader should set scope.project to itemName from routeParams FAILED
Expected undefined to equal 'foo'.
Error: Expected undefined to equal 'foo'.
As for the location dependency I tried to setUp a Mock mysel like this:
var LocationMock = function (initialPath) {
var pathStr = initialPath || '/project/bar';
this.path = function (pathArg) {
return pathArg ? pathStr = pathArg : pathStr;
};
};
Then injection $location in the before each and set a spyOn to the calling of path() like this:
spyOn(location, 'path').andCallFake(new LocationMock().path);
But then, scope.context is undefined, too.
it('should set scope.context to last part of URL', function () {
expect(scope.context).toEqual('bar');
});
Can someone please point out what I am doing wrong here?

Provider's mock works fine, but the problem is in scopes. Your directive has isolated scope. Thus this directive's scope is the child of the scope defined in test. Quick but not recomended fix is:
it('should set scope.project to itemName from $routeParams', function () {
expect(scope.$$childHead.project).toEqual('foo'); });
Try to avoid use scope when testing directives. Better approach will be to mock template and check data in it. For your case it will be something like this:
var viewTemplate = '<div>' +
'<div id="project">{{project}}</div>' +
'</div>';
beforeEach(inject(function ($templateCache) {
$templateCache.put('path/to/partials/template.html', viewTemplate);
}));
and test:
it('should set scope.project to itemName from $routeParams', function () {
expect(element.find('#project').text()).toEqual('foo');
});
for the context it will be the same.

Related

Testing $resource.get params inside a directive link function

I am trying to test a $resources service call from the link function of a directive. My test is looking to see if the service is called with the correct parameter ($stateParams.id).
The Service:
AppServices.factory('lastReportService', ['$resource',function($resource){
return $resource('/endpoint/:id/report/',null, { id : '#id'})
}]);
The Directive:
AppDirectives.directive('directive', ['lastReportService','$stateParams', function(lastReportService,$stateParams) {
return {
restrict: 'E',
templateUrl:'/static/views/directives/directive.html',
scope:{
object : '=',
},
link: function(scope, element, attrs) {
lastReportService.get({id:$stateParams.id},
function(response){ //DO STUFF WITH RESPONSE });
});
}
}}]);
The Specs:
beforeEach(function() {
inject(function ($compile, $rootScope, _$q_, lastReportService) {
compile = $compile;
scope = $rootScope.$new()
object = {"id":"cd625c6e-944e-478e-b0f1-161c025d4e1a"};
$stateParams = {"id":"cd625c6e-944e-478e-b0f1-161c025d4e1a"};
$serviceForlastReportService = lastReportService;
//Service Spy
var lastReportGetDeferred = _$q_.defer();
spyOn($serviceForlastReportService, 'get').and.callFake(function(){
lastReportGetDeferred.promise.then(this.get.arguments[1]);
return {$promise: lastReportGetDeferred.promise};
});
lastReportGetDeferred.resolve({report:'data'});
adGroupTopNav = compile(angular.element('<directive object="object"></directive>'))(scope);
scope.$digest();
});
});
it('should fetch last report service api to retrieve the last report information', function(){
expect($serviceForlastReportService.get).toHaveBeenCalledWith({id:$stateParams.id});
});
When running this test I am getting the following error.
Expected spy get to have been called with [ Object({ id: 'cd625c6e-944e-478e-b0f1-161c025d4e1a' }) ] but actual calls were [ Object({ id: undefined }) ].
So, here's my question, why the service is not called with $stateParams.id ?
Did I missed something on the Spy configuration ? Should I inject $statePArams differently ?
I think you have to inject your mock $stateParams object to your factory. Haven't tried this, but this could work in your spec
$provide.value('$stateParams',object);
This should inject your object with mock id when $stateParams is injected in the service

How to test angular directive scope: true

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

$timeout, triggering app initialization in angular directive test and $httpBackend mock error

I'm having an issue with a test (Karma+Mocha+Chai). I'm testing a pretty simple directive, part of a bigger angular module (webapp). The issue is that, when calling $timeout.flush() in my test, the module/app get's initialized and makes a request to get the template for the homepage. As $httpBackend (part of ng-mock) is not expecting any request it fails:
Unexpected request: GET /partials/homepage
No more request expected
$httpBackend#/Users/doup/Sites/projects/visitaste-web/bower_components/angular-mocks/angular-mocks.js:1208:1
...
continues
How is possible that a directive is triggering the module initialization?? Any idea how to avoid this issue? Preferably without cutting this code into another module.
Thanks!
Here the directive:
module.exports = ['$timeout', function ($timeout) {
return {
restrict: 'A', // only for attribute names
link: function ($scope, element, attrs) {
$scope.$on('vtFocus', function (event, id) {
if (id === attrs.vtFocus) {
$timeout(function () {
element.focus();
}, 0, false);
}
});
}
};
}];
And here the actual test:
describe('vtFocus', function() {
var $scope, $timeout, element;
beforeEach(module('visitaste'));
beforeEach(inject(function ($injector, $compile, $rootScope) {
$scope = $rootScope.$new();
$timeout = $injector.get('$timeout');
element = angular.element('<input vt-focus="my-focus-id"/>');
$compile(element)($scope);
}));
it('should focus the element when a vtFocus event is broadcasted with the correct focus ID', function () {
expect(element.is(':focus')).to.be.false;
$scope.$broadcast('vtFocus', 'my-focus-id');
$timeout.flush();
expect(element.is(':focus')).to.be.true;
});
it('should NOT focus the element when a vtFocus event is broadcasted with a different focus ID', function () {
expect(element.is(':focus')).to.be.false;
$scope.$broadcast('vtFocus', 'wrong-id');
$timeout.flush();
expect(element.is(':focus')).to.be.false;
});
});
This is the part where I configure UI-Router for path / in app.config():
// ...
$stateProvider
.state('homepage', {
url: '/',
templateUrl: '/partials/homepage',
});
// ...
As a workaround, I just moved the directives into it's own module visitaste.directives and loaded that module in the test, so now it's sepparated from UI-Router and it doesn't trigger a request to the template.
Still I'll wait for another solution, before I accept this answer.
describe('vtFocus', function() {
var $scope, $timeout, element;
beforeEach(module('visitaste.directives'));
beforeEach(inject(function ($compile, $rootScope, _$timeout_) {
$scope = $rootScope.$new();
$timeout = _$timeout_;
element = angular.element('<input vt-focus="my-focus-id"/>');
element.appendTo(document.body);
$compile(element)($scope);
}));
afterEach(function () {
element.remove();
});
it('should focus the element when a vtFocus event is broadcasted with the correct focus ID', function () {
expect(document.activeElement === element[0]).to.be.false;
$scope.$broadcast('vtFocus', 'my-focus-id');
$timeout.flush();
expect(document.activeElement === element[0]).to.be.true;
});
it('should NOT focus the element when a vtFocus event is broadcasted with a different focus ID', function () {
expect(document.activeElement === element[0]).to.be.false;
$scope.$broadcast('vtFocus', 'wrong-id');
expect(document.activeElement === element[0]).to.be.false;
});
});

isolateScope() returns undefined when using templateUrl

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();
});
});

Testing angularjs directive with dependencies

I'm new to AngularJs and having problem trying to test a directive with dependency (although the directive itself works as expected). I was unable to find any answers here or on the other resources.
Here is my code:
Directive:
angular.module('MyApp')
.directive('appVersion', ['config', function (config) {
return function (scope, elm) {
elm.text(config.version);
};
}]);
Service (value):
angular.module('MyApp')
.value('config', {
version: '0.1'
});
Test:
describe('Directive: AppVersion', function () {
beforeEach(module('MyApp'));
var element;
it('should have element text set to config value', inject(function ($rootScope, $compile, config) {
var scope = $rootScope;
element = $compile('<app-version></app-version>')(scope);
expect(element.text()).toBe(config.version);
}));
});
My test is failing with message:
Error: Expected '' to be '0.1'.
meaning that config value got injected properly, but $complile was not using it. I would really appreciate any help on this. Thanks.
You didn't specify the restrict attribute of the directive.
When you don't specify it, that means angular looks for app-version declared as an attribute, not an element.
So you can either add the restrict attribute to the directive or change your template :
element = $compile('<div app-version></div>')(scope);

Resources