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
Related
I'm trying to do unit testing on a directive that calls a service to retrieve posts and am confused on how to test the directive. Usually directive testing is easy with just compiling the directive element but this directive element is calling posts via a get call. How do I test this?
(function() {
'use strict';
describe('Post directive', function() {
var element, scope, postService, $rootScope, $compile, $httpBackend;
beforeEach(module('madkoffeeFrontend'));
beforeEach(inject(function(_postService_, _$rootScope_, _$compile_, _$httpBackend_) {
postService = _postService_;
$httpBackend = _$httpBackend_;
$rootScope = _$rootScope_;
$compile = _$compile_;
// $httpBackend.whenGET('http://madkoffee.com/wp-json/wp/v2/posts?per_page=3').passThrough();
scope = $rootScope.$new();
spyOn(postService, 'getPosts');
element = $compile('<posts post-num="3"></posts>')(scope);
scope.$digest();
}));
it('should get the posts successfully', function () {
expect(postService.getPosts).toHaveBeenCalled();
});
// it('should expect post to be present', function () {
// expect(element.html()).not.toEqual(null);
// });
});
})();
This is the controller:
(function() {
'use strict';
angular
.module('madkoffeeFrontend')
.directive('posts', postsDirective);
/** #ngInject */
function postsDirective() {
var directive = {
restrict: 'E',
scope: {
postNum: '='
},
templateUrl: 'app/components/posts/posts.html',
controller: PostController,
controllerAs: 'articles',
bindToController: true
};
return directive;
/** #ngInject */
function PostController($log, postService) {
var vm = this;
postService.getPosts(vm.postNum).then(function(data) {
$log.debug(data);
vm.posts = data;
}).catch(function (err) {
$log.debug(err);
});
}
}
})();
Don't call getPosts() from your test: that doesn't make sense.
Tell the spied service what to return. The fact that it uses http to get posts is irrelevant for the directive. You're unit-testing the directive, not the service. So you can just assume the service returns what it's supposed to return: a promise of posts.
Tell the spied postService what to return:
spyOn(postService, 'getPosts').and.returnValue(
$q.when(['some fake post', 'some other fake post']));
trying to access the $http service response in controller part of the directive and store in $scope object of controller which is not happening,
and I want to access this scope variable in directive's link function
Below is the directive code
angular.module('baseapp')
.directive('renderTable',['loadService',function(loadService){
return{
scope:{},
replace:true,
controller:['$scope',function($scope){
$scope.productsList={};
$scope.init=function(){
//$scope.productsList = loadService.productList;
$scope.getAllProducts();
}
$scope.getAllProducts=function(){
loadService.getData().then(function(response){ ---
*//here I am getting the response and trying to store it in $scope which is not happening*
$scope.productsList = response.products;
});
}
$scope.init();
console.log("$scope.productsList"+JSON.stringify($scope.productsList));
}],
link:function(scope,elem,attrs){
console.log("scope.productsList"+JSON.stringify(scope.productsList));
}
}
}]);
After debugging the code what I found is before the $http code call is executing, the entire code of the directive is getting executed (asynchronous), so I am not able to store it in the variable.
For this checked some posts in the same blog found suggestions to use like promises, implemented them also, but facing the same problem, some posts showed some timers which is not working....
changed the code in all the ways I could do yet not working
Could you please suggest me some workouts for the solution
Below is the service code which is working fine I am able to inject the dependency in directive
angular.module('baseapp')
.service('loadService',function($http, $q){
var loadService = this;
loadService.productList = {};
return{
getData : function(){
return $http.get('./resources/js/products.json')
}
}
});
You can do it with $rootScope and NgModelController.
Your directive could look like:
app.directive('renderTable', function () {
return {
restrict: 'EA',
replace: true,
scope: {},
require: 'ngModel',
controller: function ($rootScope, $http) {
$http.get('products.json').success(function(resp) {
$rootScope.products = resp;
console.log("directive's controller: " + JSON.stringify($rootScope.products));
});
},
link: function (scope, element, attrs, ngModelCtrl) {
ngModelCtrl.$formatters.push(function(modelValue) {
return modelValue;
});
ngModelCtrl.$render = function () {
var products = ngModelCtrl.$viewValue;
console.log("link function: " + JSON.stringify(products));
}
}
}
});
Working plunk here
See NgModelController docs
Try $scope.productsList = response.data;
I have a directive that accesses the $routeParams of the page as such:
myApp.directive("myList", function ($routeParams) {
return {
restrict: 'E',
templateUrl: 'tabs/my-list.html',
link: function (scope) {
scope.year = $routeParams.year;
}
};
});
The directive works as expected and correctly accesses the $routeParams
I am trying to test using angular-mock/jasmine. I can't figure out how to pass mock $routeParams to the directive. This is what I have:
describe('myList', function () {
var scope, compile, element, compiledDirective;
var mockParams = { 'year': 1996 };
beforeEach(function () {
module('templates', 'MyApp');
inject(function ($compile, $rootScope, $routeParams) {
compile = $compile;
scope = $rootScope.$new();
});
element = angular.element('<my-list></my-list>');
compiledDirective = compile(element)(scope);
scope.$digest();
});
it('should fill in the year', function () {
expect(scope.year).toEqual(mockParams.year);
});
});
Which obviously doesn't work because I never passed passed mockParams to the directive. Is there a way to do this?
Mock the $routeParams object mockParams using angular.extend OR do assign mockParams object directly to $routeParams. In that way $routeParams will be available before directive gets compiled.
inject(function ($compile, $rootScope, $routeParams) {
compile = $compile;
scope = $rootScope.$new();
angular.extend($routeParams, mockParams);
});
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.
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();
});
});