I have the following code for a directive using a separated controller with the "controller as" syntax:
'use strict';
angular.module('directives.featuredTable', [])
.controller('FeaturedTableCtrl',
['$scope',
function ($scope){
var controller = this;
controller.activePage = 1;
controller.changePaginationCallback =
controller.changePaginationCallback || function(){};
controller.density = 10;
controller.itemsArray = controller.itemsArray || [];
controller.metadataArray = controller.metadataArray || [];
controller.numberOfItems = controller.numberOfItems || 0;
controller.numberOfPages = 1;
controller.options = controller.options || {
'pagination': false
};
controller.changePaginationDensity = function(){
controller.activePage = 1;
controller.numberOfPages =
computeNumberOfPages(controller.numberOfItems, controller.density);
controller.changePaginationCallback({
'page': controller.activePage,
'perPage': controller.density
});
};
controller.getProperty = function(object, propertyName) {
var parts = propertyName.split('.');
for (var i = 0 ; i < parts.length; i++){
object = object[parts[i]];
}
return object;
};
controller.setActivePage = function(newActivePage){
if(newActivePage !== controller.activePage &&
newActivePage >= 1 && newActivePage <= controller.numberOfPages){
controller.activePage = newActivePage;
controller.changePaginationCallback({
'page': controller.activePage,
'perPage': controller.density
});
}
};
initialize();
$scope.$watch(function () {
return controller.numberOfItems;
}, function () {
controller.numberOfPages =
computeNumberOfPages(controller.numberOfItems, controller.density);
});
function computeNumberOfPages(numberOfItems, density){
var ceilPage = Math.ceil(numberOfItems / density);
return ceilPage !== 0 ? ceilPage : 1;
}
function initialize(){
if(controller.options.pagination){
console.log('paginate');
controller.changePaginationCallback({
'page': controller.activePage,
'perPage': controller.density
});
}
}
}]
)
.directive('featuredTable', [function() {
return {
'restrict': 'E',
'scope': {
'metadataArray': '=',
'itemsArray': '=',
'options': '=',
'numberOfItems': '=',
'changePaginationCallback': '&'
},
'controller': 'FeaturedTableCtrl',
'bindToController': true,
'controllerAs': 'featuredTable',
'templateUrl': 'directives/featuredTable/featuredTable.tpl.html'
};
}]);
You can see at the beginning of the controller that I'm initializing its properties with the attributes passed by the directive or providing default values:
controller.activePage = 1;
controller.changePaginationCallback =
controller.changePaginationCallback || function(){};
controller.density = 10;
controller.itemsArray = controller.itemsArray || [];
controller.metadataArray = controller.metadataArray || [];
controller.numberOfItems = controller.numberOfItems || 0;
controller.numberOfPages = 1;
controller.options = controller.options || {
'pagination': false
};
At the end I'm executing the initialize(); function that will execute the callback according to the options:
function initialize(){
if(controller.options.pagination){
controller.changePaginationCallback({
'page': controller.activePage,
'perPage': controller.density
});
}
}
I'm now trying to unit test this controller (with karma and jasmine) and I need to "simulate" the parameters passed by the directive, I tried the following:
'use strict';
describe('Controller: featured table', function () {
beforeEach(module('directives.featuredTable'));
var scope;
var featuredTable;
var createCtrlFn;
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
createCtrlFn = function(){
featuredTable = $controller('FeaturedTableCtrl', {
'$scope': scope
});
scope.$digest();
};
}));
it('should initialize controller', function () {
createCtrlFn();
expect(featuredTable.activePage).toEqual(1);
expect(featuredTable.changePaginationCallback)
.toEqual(jasmine.any(Function));
expect(featuredTable.density).toEqual(10);
expect(featuredTable.itemsArray).toEqual([]);
expect(featuredTable.metadataArray).toEqual([]);
expect(featuredTable.numberOfPages).toEqual(1);
expect(featuredTable.numberOfItems).toEqual(0);
expect(featuredTable.options).toEqual({
'pagination': false
});
});
it('should initialize controller with pagination', function () {
scope.changePaginationCallback = function(){};
spyOn(scope, 'changePaginationCallback').and.callThrough();
scope.options = {
'pagination': true
};
createCtrlFn();
expect(featuredTable.activePage).toEqual(1);
expect(featuredTable.changePaginationCallback)
.toEqual(jasmine.any(Function));
expect(featuredTable.density).toEqual(10);
expect(featuredTable.itemsArray).toEqual([]);
expect(featuredTable.metadataArray).toEqual([]);
expect(featuredTable.numberOfPages).toEqual(1);
expect(featuredTable.numberOfItems).toEqual(0);
expect(featuredTable.options).toEqual({
'pagination': true
});
expect(featuredTable.changePaginationCallback).toHaveBeenCalledWith({
'page': 1,
'perPage': 10
});
});
});
And got the following error, meaning that scope is not well initialized:
Expected Object({ pagination: false }) to equal Object({ pagination: true })
at test/spec/app/rightPanel/readView/historyTab/historyTab.controller.spec.js:56
Simulating the bindings would be non-trivial - after all, it's hard to really know what compiling and linking a directive does with the data passed to it...unless you just do it yourself!
The angular.js documentation offers a guide on how to compile and link a directive for unit testing - https://docs.angularjs.org/guide/unit-testing#testing-directives. After doing that, you'd just need to get the controller from the resulting element(see the documentation for the controller() method here - https://docs.angularjs.org/api/ng/function/angular.element) and perform your tests. ControllerAs would be irrelevant here - you would be testing the controller directly, instead of manipulating the scope.
Here's an example module:
var app = angular.module('plunker', []);
app.controller('FooCtrl', function($scope) {
var ctrl = this;
ctrl.concatFoo = function () {
return ctrl.foo + ' world'
}
})
app.directive('foo', function () {
return {
scope: {
foo: '#'
},
controller: 'FooCtrl',
controllerAs: 'blah',
bindToController: true,
}
})
And test setup:
describe('Testing a Hello World controller', function() {
ctrl = null;
//you need to indicate your module in a test
beforeEach(module('plunker'));
beforeEach(inject(function($rootScope, $compile) {
var $scope = $rootScope.$new();
var template = '<div foo="hello"></div>'
var element = $compile(template)($scope)
ctrl = element.controller('foo')
}));
it('should produce hello world', function() {
expect(ctrl.concatFoo()).toEqual('hello world')
});
});
(Live demo: http://plnkr.co/edit/xoGv9q2vkmilHKAKCwFJ?p=preview)
Related
this is my controller:
angular
.module('studentsApp')
.controller('StudentsController', StudentsController);
function StudentsController($scope, StudentsFactory) {
$scope.students = [];
$scope.specificStudent= {};
var getStudents = function() {
StudentsFactory.getStudents().then(function(response) {
if($scope.students.length > 0){
$scope.students = [];
}
$scope.students.push(response.data);
});
};
}
This is my factory:
angular.module('studentsApp')
.factory('StudentsFactory', function($http) {
var base_url = 'http://localhost:3000';
var studentsURI = '/students';
var studentURI = '/student';
var config = {
headers: {
'Content-Type': 'application/json'
}
};
return {
getStudents: function() {
return $http.get(base_url + studentsURI);
}
};
});
And here is how I'm trying to unit test the controller:
describe('Controller: Students', function() {
var StudentsController, scope, StudentsFactory;
beforeEach(function() {
module('studentsApp');
inject(function($rootScope, $controller, $httpBackend, $injector) {
scope = $rootScope.$new();
httpBackend = $injector.get('$httpBackend');
StudentsFactory = $injector.get('StudentsFactory');
StudentsController = $controller('StudentsController', {
$scope : scope,
'StudentsFactory' : StudentsFactory
});
students = [{
name: 'Pedro',
age: 10
}, {
name: 'João',
age: 11
}, {
name: 'Thiago',
age: 9
}];
spyOn(StudentsFactory, 'getStudents').and.returnValue(students);
});
});
it('Should get all students', function() {
scope.students = [];
StudentsController.getStudents();
$scope.$apply();
expect(scope.students.length).toBe(3);
});
});
The problem is when I run the test, the following message is displayed:
undefined is not a constructor (evaluating
'StudentsController.getStudents()')
I looked at the whole internet trying to find a tutorial that can help me on that, but I didn't find anything, could someone help me here?
It's link to the fact that the function getStudent() is private (declared by var). Thus your test can't access it. You have to attach it to the $scope or this to be able to test it.
I generally use this in controller:
var $this = this;
$this.getStudents = function() {
...
};
There's no StudentsController.getStudents method. It should be
this.getStudents = function () { ... };
Mocked StudentsFactory.getStudents returns a plain object, while it is expected to return a promise.
$controller shouldn't be provided with real StudentsFactory service as local dependency (it is already provided with it by default):
var mockedStudentsFactory = {
getStudents: jasmine.createSpy().and.returnValue($q.resolve(students))
};
StudentsController = $controller('StudentsController', {
$scope : scope,
StudentsFactory : mockedStudentsFactory
});
Here's my directive:
angular.module('app')
.directive('statusFilter',statusFilter);
function statusFilter() {
return {
restrict: 'E',
replace: true,
templateUrl: 'app/directives/status-filter.html',
scope: {
flags: '='
},
controller: function($scope, $element, $timeout, $document) {
function isChildElement(el) {
return $.contains($element[0], el);
}
function close(event) {
if (!isChildElement(event.target)) {
$scope.$apply(function() {
$scope.isOpen = false;
});
$document.off('mouseup', close);
}
}
function updateFlags(value) {
for (var prop in $scope.flagsClone) {
$scope.flagsClone[prop] = value;
}
}
function pullFlags() {
$scope.flagsClone = $.extend(true, {}, $scope.flags);
}
function pushFlags() {
for (var prop in $scope.flagsClone) {
$scope.flags[prop] = $scope.flagsClone[prop];
}
}
$scope.isOpen = false;
$scope.flagsClone = {};
pullFlags();
$scope.apply = function() {
pushFlags();
$scope.isOpen = false;
};
$scope.selectAll = function() {
updateFlags(true);
};
$scope.selectNone = function() {
updateFlags(false);
};
$scope.open = function() {
if (!$scope.isOpen) {
pullFlags();
$scope.isOpen = true;
$timeout(function() {
$document.on('mouseup', close);
});
}
};
}
};
}
Here's a simple test i wrote for it:
describe('status-filter directive', function() {
beforeEach(module('app'));
var template = '<status-filter flags="filters"></status-filter>';
var scope, element;
beforeEach(inject(function($rootScope, $compile) {
scope = $rootScope.$new();
var ngElement = angular.element(template);
element = $compile(ngElement)(scope);
scope.$digest();
}));
it('Should open when isOpen is true', function() {
scope.open();
scope.$digest();
expect(scope.isOpen).toBe(true);
});
});
I cannot access the directive's scope no matter how i try. Like in the example above, .isolateScope(), element.scope(). With anything i try i get open() is undefined error. What is wrong in my code?
The reason why I couldn't access the scope was that I didn't create the filters variable. So this will work:
describe('status-filter directive', function() {
beforeEach(module('app'));
var template = '<status-filter flags="filters"></status-filter>';
var scope, element;
beforeEach(inject(function($rootScope, $compile) {
scope = $rootScope.$new();
scope.filters = {
filter1:true;
}
var ngElement = angular.element(template);
element = $compile(ngElement)(scope);
scope.$digest();
}));
it('Should open when isOpen is true', function() {
scope.open();
scope.$digest();
expect(scope.isOpen).toBe(true);
});
});
here is the code:
(function(){
"use strict";
angular.module("dataModule")
.controller("panelController", ["$scope", "$state", "$timeout", "$modal", panelController]);
function panelController($scope, $state, $timeout, $modal){
$scope.property = "panelController";
//how do we do unit test on openCancelWarning.
//i did not find a way to get openCancelWarning function in Jasmine.
function openCancelWarning () {
var cancelModal = $modal.open({
animation: true,
backdrop: "static",
templateUrl: "pages/data/cancel-warning.html",
controller: "cancelWarningController",
size: "sm",
resolve: {
items : function() {
return {
warningTitle : "Are you Sure?",
warningMessage: "There are unsaved changes on this page. are you sure you want to navigate away from this page?Click OK to continue or Cancel to stay on this page"
};
}
}
});
return cancelModal;
}
var resultPromise = openCancelWarning();
var result;
resultPromise.result.then(function(response){
result = response;
});
}
angular.module("dataModule")
.controller("cancelWarningController", ["$scope", "$modalInstance", "items", cancelWarningController]);
function cancelWarningController($scope, $modalInstance, items){
$scope.warningTitle = items.warningTitle;
$scope.warningMessage = items.warningMessage;
$scope.cancel = function() {
$modalInstance.close(false);
};
$scope.ok = function() {
$modalInstance.close(true);
};
}
}());
here is my jasmine unit test code.
describe("Controller: panelController", function () {
beforeEach(module("dataModule"));
var panelController, scope;
var fakeModal = {
result : {
then: function(confirmCallback) {
this.confirmCallback = confirmCallback;
}
},
close: function(confirmResult) {
this.result.confirmCallback(confirmResult);
}
};
beforeEach(inject(function($modal) {
spyOn($modal, "open").andReturn(fakeModal);
}));
beforeEach(inject(function ($controller, $rootScope, _$modal_) {
scope = $rootScope.$new();
panelController = $controller("panelController", {
$scope: scope,
$modal: _$modal_
});
}));
it('test should be true', function () {
var test;
var testResult = panelController.openCancelWarning();
testResult.close(true);
testResult.then(function(response){
test=response;
});
expect(test).toBe(true);
});
});
i wrote above unit test code with the help from Mocking $modal in AngularJS unit tests
i always get below error.
TypeError: 'undefined' is not a function (evaluating 'panelController.openCancelWarning()')
could anyone help this?
I have created a directive below:
html:
<div image-upload></div>
directive:
angular.module('app.directives.imageTools', [
"angularFileUpload"
])
.directive('imageUpload', function () {
// Directive used to display a badge.
return {
restrict: 'A',
replace: true,
templateUrl: "/static/html/partials/directives/imageToolsUpload.html",
controller: function ($scope) {
var resetScope = function () {
$scope.imageUpload = {};
$scope.imageUpload.error = false;
$scope.imageUpload['image_file'] = undefined;
$scope.$parent.imageUpload = $scope.imageUpload
};
$scope.onImageSelect = function ($files) {
resetScope();
$scope.imageUpload.image_file = $files[0];
var safe_file_types = ['image/jpeg', 'image/jpg']
if (safe_file_types.indexOf($scope.imageUpload.image_file.type) >= 0) {
$scope.$parent.imageUpload = $scope.imageUpload
}
else {
$scope.imageUpload.error = true
}
};
// Init function.
$scope.init = function () {
resetScope();
};
$scope.init();
}
}
});
This directive works fine and in my controller I access $scope.imageUpload as I required.
Next, I tried to pass into the directive a current image but when I do this $scope.imageUpload is undefined and things get weird...
html:
<div image-upload current="project.thumbnail_small"></div>
This is the updated code that gives the error, note the new current.
angular.module('app.directives.imageTools', [
"angularFileUpload"
])
.directive('imageUpload', function () {
// Directive used to display a badge.
return {
restrict: 'A',
replace: true,
scope: {
current: '='
},
templateUrl: "/static/html/partials/directives/imageToolsUpload.html",
controller: function ($scope) {
var resetScope = function () {
$scope.imageUpload = {};
$scope.imageUpload.error = false;
$scope.imageUpload['image_file'] = undefined;
$scope.$parent.imageUpload = $scope.imageUpload
if ($scope.current != undefined){
$scope.hasCurrentImage = true;
}
else {
$scope.hasCurrentImage = true;
}
};
$scope.onImageSelect = function ($files) {
resetScope();
$scope.imageUpload.image_file = $files[0];
var safe_file_types = ['image/jpeg', 'image/jpg']
if (safe_file_types.indexOf($scope.imageUpload.image_file.type) >= 0) {
$scope.$parent.imageUpload = $scope.imageUpload
}
else {
$scope.imageUpload.error = true
}
};
// Init function.
$scope.init = function () {
resetScope();
};
$scope.init();
}
}
});
What is going on here?
scope: {
current: '='
},
Everything works again but I don't get access to the current value.
Maybe I'm not using scope: { correctly.
in your updated code you use an isolated scope by defining scope: {current: '=' } so the controller in the directive will only see the isolated scope and not the original scope.
you can read more about this here: http://www.ng-newsletter.com/posts/directives.html in the scope section
I've got directive and service in my app (declared in separate files):
Service:
(function(){
angular.module('core', [])
.factory('api', function() {
return {
serviceField: 100
};
})
})();
Directive:
(function(){
angular.module('ui', ['core'])
.directive('apiFieldWatcher', function (api) {
return {
restrict: 'E',
replace: true,
scope: true,
template: '<div>+{{apiField}}+</div>',
controller: function($scope) {
$scope.apiField = 0;
},
link: function (scope) {
scope.$watch(function(){return api.serviceField}, function(apiFld){
scope.apiField = apiFld;
});
}
}
});
})();
And in another separate file I have native model:
function Model() { this.fld = 0; }
Model.prototype.setFld = function(a) { this.fld = a; }
Model.prototype.getFld = function() { return this.fld; }
How can I bind (two way) my native this.fld field to value in my AngularJS service?
The solution is in using this code:
Model.prototype.setFld = function(a) {
this.fld = a;
injector.invoke(['$rootScope', 'api', function($rootScope, api){
api.setField(a);
$rootScope.$digest();
}]);
};
Model.prototype.getFldFromApi = function() {
var self = this;
injector.invoke(['api', function(api){
self.fld = api.getField();
}]);
};
http://plnkr.co/edit/nitAVuOtzGsdJ49H4uyl
i think it's bad idea to use $digest on $rootScope, so we can maybe use
var scope = angular.element( elementObject ).scope();
to get needed scope and call $digest for it