Instantiating a controller that uses an isolate scope for a test - angularjs

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.

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

How to mock one-way binding parameters with $controller decorator?

I am writing tests for a directive's controller whose startup logic requires some one-way bound parameters to be defined. I am facing difficulties passing one-way bound parameters to the mocked controller. text-bound parameters are passed without difficulty.
NOTE: In the following code, I skip module injections and other irrelevant parts of the code
Here is the directive's definition with two types of parameters:
function DummyDirective() {
return {
restrict: 'E',
scope: {},
bindToController:
oneWay: '&',
text: '#'
},
templateUrl: 'path/to/template.html',
controller: 'GreatController',
controllerAs: 'vm'
};
}
And here is the controller:
function GreatController() {
var vm = this;
vm.treasure = '';
activate();
function activate() {
vm.treasure = fantastic(vm.oneWay);
}
function fantastic(crap) {
if(crap.stack) return 'gold';
}
}
As for the test:
var controller, $rootScope, $scope;
beforeEach(inject(function(_$controller_, _$rootScope_){
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
controller = _$controller_(
'GreatController',
{$scope: $scope},
{text: 'toGoodToBeTrue',
oneWay: {
stack: 'lotsOfIt',
}
}
);
}));
it('should do the job', function(){
expect(controller.treasure).toBe('gold');
});
While text is properly passed to $controller, oneWay is undefined and the test fails. I have also tried to pass it a function returning an object to no avail. The documentation does not cover one-way bound parameters. Would you know how to do this without testing the directive and passing parameters as markup? In general I like to keep my tests focused and test directives and their controllers separately.
What if make oneWay optional, adding question mark to its definition:
oneWay: '&?'

Testing Directive Angular using ControllerAs

I'm trying to test a directive. using controllerAs creates a toplevel scope where we can access its properties. however when debugging I try to access element.scope().property --I am getting undefined. Any help on why would be greatly appreciated.
--BugDirective
(function() {
'use strict';
angular
.module('debug-blog-app')
.directive('avBug', avBug);
function avBug() {
return {
restrict: 'E',
templateUrl: 'views/directives/bug.html',
scope: {
bug: '='
},
controller: BugFormController,
controllerAs: 'bugCtrl',
bindToController: true
};
};
BugFormController.$inject = ['$window', '$scope', 'BugService'];
function BugFormController($window, $scope, BugService) {
var vm = this;
vm.updateBug = function(){
BugService.updateBug(vm.bug);
};
vm.deleteBug = function(){
if($window.confirm("Delete the Bug?!")){
return BugService.deleteBug(vm.bug.id)
.then(function(){
$scope.$emit('bug.deleted', vm.bug);
});
}
};
};
})();
--Spec
'use strict'
describe('avBug Directive', function () {
var bugCtrl,
element,
BugService,
$scope,
$rootScope;
beforeEach(module('app'));
beforeEach(inject(function($q, $compile, _$rootScope_, _BugService_) {
$rootScope = _$rootScope_;
var directiveMarkup = angular.element("<av-bug></av-Bug>");
element = $compile(directiveMarkup)($rootScope);
bugCtrl = element.scope().bugCtrl;
BugService = _BugService_;
spyOn(BugService, 'deleteBug').and.callFake(function() {
var deferred = $q.defer();
deferred.resolve('data');
return deferred.promise;
});
spyOn($rootScope,'$emit').and.callThrough();
}));
it('should delete a bug', function() {
bugCtrl.deleteBug(0);
expect(BugService.deleteBug).toHaveBeenCalledWith(0);
$rootScope.$digest();
expect($rootScope.$emit).toHaveBeenCalledWith('bug.deleted');
});
});
--index.html
<div class="container">
<div ui-view></div>
</div>
--home.html
<av-bug bug="bug" ng-repeat="bug in homeCtrl.bugs"></av-bug>
I would also add, for unit testing purposes, that you can get the parent controllerAs with the same function that #Ramy Deeb post.
vm = element.scope().$$childTail.nameOfParentControllerAs
And for testing the element isolate scope, just get
isolateScope = element.isolateScope()
I hope this can help anyone like me ending here searching how to unit test directives and controllers calling them, all of them being using ControllerAs syntax.
Thanks you.
I would comment, but I cannot.
Your controller is located at:
element.scope().$$childTail.bugCtrl

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

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

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

Resources