Angular unit testing: Argument 'fn' is not a function, got Object - angularjs

This error is occurring when I try to instantiate the setup phase of my unit test. I am unit testing a directive, which has its own controller. For best practice purposes I can always add the controllerAs property to the directive to assign the controller a name, but I get the same error if I do that anyway.
describe('myDirective', function() {
beforeEach(module('app'));
beforeEach(module('app/directives/directive.html'));
var theArgs = {
arg1 : [],
arg2 : 'id',
arg3 : [],
arg4 : '',
arg5 : false
};
beforeEach(inject(function($templateCache, _$compile_, _$rootScope_, $controller) {
template = $templateCache.get('app/directives/directive.html');
$templateCache.put('app/directives/directive.html', template);
$compile = _$compile_;
$rootScope = _$rootScope_;
scope = $rootScope.$new();
scope.args = theArgs;
ctrl = $controller({
$scope: scope
});
}));
it('should compile', function() {
var myElement = angular.element('<div my-directive args="theArgs" ></div>');
var element = $compile(myElement)(scope);
// Grab scope. Depends on type of scope.
scope = element.isolateScope() || element.scope();
// Grab controller instance
controller = element.controller(ctrl);
$rootScope.$digest();
// Mock the directive's controller's add() function
scope.add();
});
});
The error is occurring within this block:
ctrl = $controller({
$scope: scope
});
Since the controller doesn't have a name, I am not passing it one in the above code block. That shouldn't throw an error by itself though, right? I don't think there is a problem with my karma configuration since my other 500 tests are all passing.
The second error is being thrown at controller = element.controller(ctrl);, where it can't find the ctrl variable. That error makes sense because it's caused by the first error, but I can't figure out how to fix the first error.
UPDATE: Added directive code to show how the controller was defined. It was never assigned a name, it is anonymous, and I didn't use the controllerAs property, because it returns an error.
app.directive('myDirective', function() {
var dirController = ['$scope', function($scope) {
$scope.add = function() { ... };
}];
return {
restrict: 'A',
scope: {
args: '='
},
templateUrl: '/path/to/template.html',
controller: dirController
};
});

Well the problem is exactly with this code section:
ctrl = $controller({
$scope: scope
});
Since $controller is require the name of the controller for the first parameter, and then the injectables afterwards within an object literal.
E.g.: Tell it which controller should it create:
ctrl = $controller('MyControllerName', {
$scope: scope
});

Related

angularjs directive unit test fail with controllerAs, bindToController & isolateScope()

I am trying to unit test a directive with a two-way bound property (=). The directive works in my app, but I can't get a unit test working that tests the two-way binding.
I have been trying to get this working for days. I've read MANY examples that use some but not all of the features I want to use: controllerAs, bindToController & isolateScope(). (Forget about templateURL, which I also need. I will add that if I can get this working! :)
I'm hoping someone can tell me how to show a change in the parent scope reflected in the isolate scope.
Here is a plunkr that contains the code below:
http://plnkr.co/edit/JQl9fB5kTt1CPtZymwhI
Here is my test app:
var app = angular.module('myApp', []);
angular.module('myApp').controller('greetingController', greetingController);
greetingController.$inject = ['$scope'];
function greetingController($scope) {
// this controller intentionally left blank (for testing purposes)
}
angular.module('myApp').directive('greetingDirective',
function () {
return {
scope: {testprop: '='},
restrict: 'E',
template: '<div>Greetings!</div>',
controller: 'greetingController',
controllerAs: 'greetingController',
bindToController: true
};
}
);
And here is the spec:
describe('greetingController', function () {
var ctrl, scope, rootScope, controller, data, template,
compile, isolatedScope, element;
beforeEach(module('myApp'));
beforeEach(inject(function ($injector) {
rootScope = $injector.get('$rootScope');
scope = rootScope.$new();
controller = $injector.get('$controller');
compile = $injector.get('$compile');
data = {
testprop: 1
};
ctrl = controller('greetingController', {$scope: scope}, data);
element = angular.element('<greeting-directive testprop="testprop"></greeting-directive>');
template = compile(element)(scope);
scope.$digest();
isolatedScope = element.isolateScope();
}));
// PASSES
it('testprop inital value should be 1', function () {
expect(ctrl.testprop).toBe(1);
});
// FAILS: why doesn't changing this isolateScope value
// also change the controller value for this two-way bound property?
it('testprop changed value should be 2', function () {
isolatedScope.testprop = 2;
expect(ctrl.testprop).toBe(2);
});
});
You have to correct the way you're testing your directive. You're directly changing isolatedScope of an object and thereafter verifying the ctrl object which unrelated DOM which you had compiled.
Basically what you should be doing is as soon as you compiled a DOM with scope (here it is <greeting-directive testprop="testprop"></greeting-directive>). So that scope will hold the context of compiled do. In short you can play testprop property value. or same thing will be available inside element.scope(). As soon as you change any value in scope/currentScope. You can see the value gets updated inside directive isolatedScope. One more thing I'd like to mention is when you do controllerAs with bindToController: true, angular adds property with controller alias inside scope that's we verified isolatedScope.greetingController.testprop inside assert
Code
describe('greetingController', function() {
var ctrl, scope, rootScope, controller, data, template,
compile, isolatedScope, currentScope, element;
beforeEach(module('myApp'));
beforeEach(inject(function($injector) {
rootScope = $injector.get('$rootScope');
scope = rootScope.$new();
controller = $injector.get('$controller');
compile = $injector.get('$compile');
data = { testprop: 1 };
ctrl = controller('greetingController', { $scope: scope }, data);
element = angular.element('<greeting-directive testprop="testprop"></greeting-directive>');
template = compile(element)(scope);
scope.$digest();
currentScope = element.scope();
//OR
//currentScope = scope; //both are same
isolatedScope = element.isolateScope();
}));
// First test passes -- commented
it('testprop changed value should be 2', function() {
currentScope.testprop = 2; //change current element (outer) scope value
currentScope.$digest(); //running digest cycle to make binding effects
//assert
expect(isolatedScope.greetingController.testprop).toBe(2);
});
});
Demo Plunker

Instantiating a controller that uses an isolate scope for a test

So I have a controller test file with:
scope = $rootScope.$new();
ctrlInstance = $controller( 'formCtrl', { $scope: scope } );
This controller isn't getting instantiated correctly, because the scope that I'm passing in doesn't have data that it normally has (due to being passed from an isolate scope).
These are the first few lines of my formCtrl:
var vm = this;
vm.stats = angular.copy( vm.statsStuff );
vm.stats.showX = vm.stats.showY = true;
Note that vm.statsStuff has data bound to it (due to a '=' scope in the corresponding directive), but I'm not sure how to pass it these values when I instantiate my controller in the test.
Any help would be appreciated.
Adding directive:
angular.module( 'myModule' )
.directive( 'formStuff', function() {
return {
restrict: 'E',
templateUrl: 'dir.tpl.html',
scope: {
statsStuff: '='
},
controller: 'formStuffCtrl',
controllerAs: 'formCtrl',
bindToController: true
};
} );
})();
The angular-mocks module has a $controller service that decorates the "real" one, and allows passing a third argument, containing data to bind to the controller before instantiating it.
So all you should need is
ctrlInstance = $controller('formCtrl', { $scope: scope }, { statsStuff: theStuff } );
Until you upgrade to 1.4 (when doing so, JB's answer is the way), I would do the following to "emulate" what the third parameter is doing (to some extent*):
var $scope, ctrlInstance, createController;
beforeEach(function () {
module('your_module');
inject(function ($injector, $controller) {
$scope = $injector.get('$rootScope').$new();
createController = function (bindStuff) {
ctrlInstance = $controller('formStuffCtrl', {
$scope: $scope
});
Object.keys(bindStuff).forEach(function (key) {
ctrlInstance[key] = bindStuff[key];
});
});
});
});
it('exposes the "statsStuff stuff"', function () {
var stats = { x: 500, y: 1000 };
createController({ stats: stats });
expect(ctrlInstance.stats).to.deep.equal(stats);
});
Even without the bindToController 'emulation', I would highly recommend the createController way of instantiating your controller as it gives you the flexibility of manipulating the controllers dependencies before hand (without the need of another before|beforeEach block).
*: I say to some extent, as this is attaching the properties after the controller has been instantiated, whereas bindToController attaches the properties before hand. So there may very well be some discrepancies between the two.

How to get angular controller reference from compiled element in unit tests?

After giving up on ngMockE2E being able to supply proper passThrough() from the $httpBackend, I've resorted to adding the templates to the cache manually. This seems to work great, if I want to just 'test' my template. However, say I want to test some conditions on the controller that get invoked (via directive), how do a I get a reference to it?
describe('RedactedFilter ', function() {
var ctrl, filterModel,createController;
var $httpBackend,$scope,template, $templateCache,$compile,$rootScope, $compile;
beforeEach(module('RedactedApp.Services'));
beforeEach(module('RedactedApp.Controllers'));
beforeEach(module('RedactedApp.Models'));
beforeEach(inject(function($templateCache,_$compile_,_$rootScope_) {
//assign the template to the expected url called by the directive and put it in the cache
template = $templateCache.get('src/main/app/components/redactedFilter/redacted-filter.tpl.html');
$templateCache.put('src/main/app/components/redactedFilter/redactedFilter-filter.tpl.html',template);
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
beforeEach(inject(function ($controller, $rootScope,_$q_, $injector, _$compile_) {
$scope = $rootScope.$new();
$httpBackend = $injector.get('$httpBackend');
$compile = _$compile_;
filterModel = $injector.get('RedactedFilterModel');
}));
it('should default to tab 0', function() {
var formElement = angular.element('<redacted-filter></redacted-filter>');
var element = $compile(formElement)($rootScope);
$rootScope.$digest();
var ctrl = element.controller();
//ctrl is undefined
expect(ctrl.selectedTab).toEqual(0);
});
});
Note that it does not say function controller() is undefined, but the result of calling it. My directive has replace set to false so I don't think it's an issue with the transclusion hiding the element.
Here's my directive for good measure:
angular.module('RedactedApp.Directives').directive('redactedFilter', function(){
return {
restrict: 'E',
replace: false,
templateUrl: '../../main/app/components/redactedFilter/redacted-filter.tpl.html'
};
});
First, use ng-html2j or ng-templates to shove your templates into your template cache before you run tests.
Then compile the templates into elements, and shove them into your controller for testing:
beforeEach(inject(function($templateCache,_$compile_,_$rootScope_, _$controller_) {
//get the template from the cache
template = $templateCache.get('src/main/app/components/someController/widget-search.tpl.html');
$compile = _$compile_;
$rootScope = _$rootScope_;
$controller = _$controller_;
//compile it
element = $compile(template)($rootScope);
}));
//then shove your compiled element into your controller instance
it( 'should be instantiated since it does not do much yet', function(){
ctrl = $controller('SomeController',
{
$scope: $rootScope.$new(),
$element: element
});
expect( ctrl.hasInstance).toBeTruthy();
});

Testing Angular Directive with Dependencies like $location and $routeParams

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.

Injecting $attrs in AngularJS

I have some JavaScript written in the context of AngularJS. My relevant JavaScript looks like the following:
.factory('$myFactory', function ($myLibrary, $interpolate) {
return {
myFunction: function ($scope, $attrs, p) {
if (p !== null) {
$attrs.set('myProperty', p);
}
}
};
I am trying to unit test this code. In an attempt to unit test the code, I'm using Jasmine.
it('should set myProperty', inject(function ($scope, $myFactory) {
// $myFactory.myFunction($scope
}));
I can't figure out how to inject some $attrs from my unit test. How do I do that? I can successfully get my $scope setup. However, I don't understand how to inject $attrs. Is there something special about it that I'm not aware of? I'm having a similar issue with $element, though that one is out of the context of this specific test.
Thank you!
Here is a plunker: http://plnkr.co/edit/k1cxSjpAXhhJUEOcx9PG
Maybe there is a better solution but That's what I got.
$scope is easy to get, you can inject $rootScope everywhere
$attrs on the other hand is only available through the $compile variable (it lives in compile.js)
My solution is to create a fake controller , to compile it and to hijack it's $attrs.
So that's how it looks like:
var injected = null
function fakeController($scope, $attrs, $element){
injected = {}
injected.$scope = $scope;
injected.$attrs = $attrs;
injected.$element = $element;
}
describe('Testing a Hello World controller', function() {
var $scope = null;
var $attrs = null;
var $element = null;
var ctrl = null;
//you need to indicate your module in a test
beforeEach(module('plunker'));
beforeEach(inject(function($compile, $rootScope, $controller) {
$compile('<span ng-controller="fakeController"></span>')($rootScope);
$scope = injected.$scope;
$attrs = injected.$attrs;
$element = injected.$element;
ctrl = $controller('MainCtrl', {
$scope: $scope,
$attrs: $attrs,
$element: $element
});
}));
it('should say hallo to the World', function() {
expect($scope.name).toEqual('World');
});
});
seems that you can just instantiate the controller with empty object, as well...
ctrl = $controller('MyCtrl', {
$scope: $scope,
$attrs: {}
});
In latest versions of AngularJS a controller needs to be provided to get full $attrs.
See $controllerProvider documentation.
Here's a snippet
describe('angular-component-controller', function() {
// save injected parameters of controller
var injected = {};
var controller;
// #see $https://docs.angularjs.org/api/ngMock/service/$componentController
// we need to extract those from $compile instead of use as locals
angular.module('locals', []).controller('controller',
['$attrs', function($attrs) {
injected.$attrs = $attrs;
}]
);
beforeEach(module('locals'));
beforeEach(inject(function($rootScope, $compile, $componentController) {
// invoke dummy component to get $attrs
$compile('<span ng-controller="controller">')($rootScope);
var locals = {};
locals.$scope = $rootScope.$new();
locals.$attrs = injected.$attrs;
var bindings = {};
controller = $componentController('component', locals, bindings);
}));
});
See this gist AngularJS $componentController unit test

Resources