I'm using angular-datatables in our project.
I got this error while testing:
TypeError: DTOptionsBuilder.newOptions(...).withBootstrap is not a function in /var/www/symfony/xxxx/src/XXXX/AdminBundle/Resources/public/js/controllers/category/category-grid.controller.js (line 20)
Which the code is recognised as function in dev code, and here is the testing code:
(function() {
"use strict";
describe('Category Grid Controller Unit Test', function() {
var $controller, $rootScope;
beforeEach(module('category-grid.controller'));
beforeEach(inject(function(_$controller_, _$rootScope_) {
$controller = _$controller_;
$rootScope = _$rootScope_;
}));
it('should define category grid controller', function() {
var $scope = $rootScope;
var CategoryGridController = $controller('CategoryGridController', {$scope: $scope});
expect(CategoryGridController).toBeDefined();
});
});
})();
And here is the controller code:
function CategoryGridController($stateParams, $scope, Category, DTOptionsBuilder, DTColumnDefBuilder, Utils, $window, notify) {
$scope.categories = [];
$scope.dtOptions = DTOptionsBuilder.newOptions().withBootstrap();
$scope.dtColumnDefs = [
DTColumnDefBuilder.newColumnDef(5).notSortable()
];
if (angular.isDefined($stateParams.id)) {
Category.get({id: $stateParams.id}, function(data) {
var categories = data.children;
var translation;
for (var i = 0; i < categories.length; i++) {
translation = Utils.getTranslation(categories[i].translations);
categories[i].name = translation.name;
}
translation = Utils.getTranslation(data.translations);
$scope.categoryName = translation.name;
$scope.categories = categories;
});
}
else {
Category.query({ _level: 1 }, function(data) {
for (var i = 0; i < data.length; i++) {
var translation = Utils.getTranslation(data[i].translations);
data[i].name = translation.name;
}
$scope.categories = data;
});
}
$scope.toggleEnabled = function(id, index) {
console.log(!$scope.categories[index].enabled);
if ($window.confirm($scope.translations.ARE_YOU_SURE)) {
Category.patch({id: id}, {category: {enabled: !$scope.categories[index].enabled}},
function(data) {
$scope.categories[index].enabled = data.enabled;
notify({
messageTemplate: '<span><i class="fa fa-check"></i> ' + $scope.translations.DATA_SAVE_SUCCESS + '</span>'
});
},
function(e) {
notify({
messageTemplate: '<span><i class="fa fa-warning"></i> ' + $scope.translations.DATA_SAVE_ERROR + '</span>',
classes: 'alert-danger'
});
}
);
}
};
}
The controllers code work in development. If I remove the line with DTOptionsBuilder.newOptions(...).withBootstrap, the testing code works. But I need that code in development.
Since you are performing unit testing of controller, all dependencies on the controller need to mocked and injected, including your DTOptionsBuilder.
This dependency can be mocked by either creating you own custom objects and injecting them in the test or using jasmine spy.Read documentation around how to create spy in Jasmine.
For example in your test you can create
var DTOptionsBuilder={
newOptions:function() {
return {
withBootstrap :function() {}
};
}}
and inject this object when you instantiate the controller
var CategoryGridController = $controller('CategoryGridController', {$scope: $scope,'DTOptionsBuilder':DTOptionsBuilder});
The bottom line is if you controller is dependent upon any other service, either a real or mock implementation of that service has to be inject for controller to work, allowing you to unit test it.
Related
I've moved this 'reports' feature from a single module (called 'aam') into the core, so that other modules (such as 'bbc') can use it.
Now I'm rewriting the unit test(s).
The grunt error I'm getting is
should go state aam.reports with URL_NOT_SPECIFIED
reports-state spec
TypeError: 'null' is not an object
(evaluating 'BbpcConfiguration.getProperty(configProperty).then')
which indicates to me that $state is empty or not structured correctly.
Here is the report controller:
(function() {
'use strict';
angular.module('com.ct.bbpcCore')
.controller('reportController', ['$window', '$state', 'BbpcUserService', 'BbpcConfiguration', function ($window, $state, BbpcUserService,BbpcConfiguration) {
angular.element(document).ready(function () {
//Get url base on locale
var reportUrl = "URL_NOT_SPECIFIED";
var currentState = $state.current.name;
var configProperty = "";
var title = "";
if (currentState.indexOf('aam.reports')) {
configProperty = 'report.aam.link';
title = "AAM.REPORT";
};
if (currentState.indexOf('bbc.reports')) {
configProperty = 'report.bbc.link';
title = "BBC.REPORT";
};
BbpcConfiguration.getProperty(configProperty).then(function(response) {
if (response) {
var language = BbpcUserService.getLanguageCd() || "en_CA";
reportUrl = response[language] || reportUrl;
}
var spec = "width=" + $window.outerWidth + ", height=" + $window.outerHeight;
$window.open(reportUrl, title, spec);
});
});
}]);
}());
And here is report-controller.spec:
describe('reports-state spec', function() {
'use strict';
var $injector, $window, $rootScope,
$state, BbpcConfiguration, reportController, $controller, BbpcUserService;
beforeEach(function() {
module('com.ct.bbpcCore', function($provide) {
$provide.value('BbpcConfiguration', BbpcConfiguration = {
getProperty: function(key){
if('report.aam.link' === key){
return {
"fr_CA": "https://eng-link",
"en_CA": "https://fre-link"
};
}
return null;
}
});
});
inject(function(_$injector_) {
$injector = _$injector_;
$window = $injector.get('$window');
$state = $injector.get('$state');
$rootScope = $injector.get('$rootScope');
$controller =$injector.get('$controller');
BbpcUserService =$injector.get('BbpcUserService');
});
});
it('should go state aam.reports with URL_NOT_SPECIFIED', function() {
$state.current = {'name': 'aam.reports' };
spyOn($window, 'open').andCallFake(function(){});
reportController = $controller('reportController', {'$window':$window, '$state':$state, 'BbpcUserService':BbpcUserService, 'reportLink':undefined});
$state.go('aam.reports');
$rootScope.$apply();
expect($state.current.name).toEqual('aam.reports');
expect($window.open).toHaveBeenCalledWith('URL_NOT_SPECIFIED', 'AAM.REPORT', 'width=0, height=0');
});
});
I tried simply adding the line $state.current = {'name': 'aam.reports' }; in the 'it' block, but that's not what it's looking for.
Not sure how to debug unit tests. :P I can't use a console.log($state) to peek into it.
I am trying to test a controller. The controller uses a service which is using $http to get the data from a json file (This json file is just a mock up of response returned from server)
My problem is that when I am testing the controller, it creates the controller object and even calls the service. But it doesnt call the $http mocked response. I not sure where I am going wrong. I tried looking at few examples but all of them are using $q.
My service looks like this:
(function(){
angular.module('mymodule')
.factory('MyService', MyService);
MyService.$inject = ['$http'];
function MyService($http) {
var service = {
retrieveData : retrieveData
};
return service;
function retrieveData(containerLabel){
var myGrossData = [];
var isMatchFound = false;
var myindex = containerLabel.slice(-4);
return $http.get('app/myGrossData.json').then(function(response) {
console.log('inside http retrieveData: ');
myGrossData = response.data;
var myindexExists = false;
var mydataObject = [];
var defaultdata = [];
angular.forEach(myGrossData, function (myGrossData) {
if (myindex === myGrossData.myindex) {
mydataObject = myGrossData;
isMatchFound = true;
}
if(!isMatchFound && myGrossData.myindex === '2006')
{
mydataObject = myGrossData;
}
if(myGrossData.myindex === '2006'){
defaultdata = myGrossData;
}
});
if (isMatchFound && response.status === 200)
{
return mydataObject;
}
else if(!isMatchFound && (response.status === 200 || response.status === 201)){
return defaultdata;
}
else //all other responses for success block
{
return 'Incorrect Response status: '+response.status;
}
},
function(error){
return 'Error Response: '+error.status;
}
);
}
};
})();
The controller calling it is :
(function () {
'use strict';
angular
.module('mymodule', [])
.controller('MyCtrl', MyCtrl);
MyCtrl.$inject = ['$scope', 'MyService'];
function MyCtrl($scope, MyService) {
var vm = this;
vm.datafromsomewhere = datafromsomewhere;
vm.displayData = [];
vm.disableBarCode = false;
vm.childCount = 0;
vm.headertext="Master Container Builder";
init();
function init() {
console.log('MyCtrl has been initialized!');
console.log(vm.headertext);
}
function myfunctionCalledByUI(input) {
processData(input);
}
function processData(containerLabel){
MyService.retrieveMasterContainer(containerLabel).then(function(data){
vm.displayData = data;
});
vm.disableBarCode = true;
vm.childCount = (vm.displayData.childData === undefined) ? 0: vm.displayData.childData.length;
vm.headertext="Myindex "+vm.displayData.myindex;
if ( vm.displayData.masterDataId.match(/[a-z]/i)) {
// Validation passed
vm.displayData.masterDataId ="No Shipping Label Assigned";
}
else
console.log('else: '+vm.displayData.masterDataId);
console.log('length of childData: '+vm.childCount);
}
}
})();
and finally my spec looks like this:
var expect = chai.expect;
describe('Test Controller', function () {
var rootScope, compile; MyService = {};
var $scope, $controller;
beforeEach(module('ui.router'));
beforeEach(function() {
module('mymodule');
inject(function ($rootScope, _$compile_,_$controller_) {
rootScope = $rootScope;
compile = _$compile_;
$scope = $rootScope.$new();
MyService = jasmine.createSpyObj('MyService', [
'retrieveData'
]);
$controller = _$controller_('MyCtrl', {
$scope: $scope
});
});
});
it('controller should be initialized and data should also be initialized', function() {
expect($controller).to.not.be.undefined;
expect($controller).to.not.be.null;
expect($controller.disableBarCode).to.equal(false);
expect($controller.childCount).to.equal(0);
expect($controller.headertext).to.equal("Master Container Builder");
});
it(' should process data when containerLabel is called into myfunction', function() {
$controller.handKeyed('12001');
expect(MyService.retrieveData).to.have.been.called;
expect($controller.processData).to.have.been.called;
expect($controller.disableBarCode).to.equal(true);
expect($controller.childCount).to.equal(0);
expect($controller.headertext).to.equal("Master Container Builder");
});
});
I am using following techstack if it helps:
angular 1.5
Ionic
Karma-jasmine
The code works when I run it. My issue is that when i run the test it doesnt populate the data in my vm.displayData variable. how do I make it get some data into the service. I added in some log statements and it skips it completely.
After all the test run including unrelated tests to this one, then I see the log statements from MyService. I am not sure how to approach this.
I think what you are looking for is the $httpBackend service. It will mock the request indicating the result. So, when your service hit the url, it will return what you passed to the $httpBackend configuration.
A simple example would be:
it('should list newest by category', function(){
$httpBackend
.expectGET(url)
.respond(techPosts /*YOUR MOCKED DATA*/);
$stateParams.category = 'tech';
var controller = $controller('HomeCtrl', { PostsResource: PostsResource, $stateParams: $stateParams });
controller.listNewestPosts();
$httpBackend.flush();
expect(controller.posts).toEqual(techPosts.posts);
});
I have a service that adds/removes classes to a couple of HTML elements.
I am trying to test these changes depending on which method is called.
define(['require', 'angular'], function (require, angular) {
'use strict';
var myFactory = function () {
var header = angular.element("#app-header");
var footer = angular.element(document.getElementsByClassName("app-footer"));
var change = false;
return {
red: function() {
header.addClass("alert-warning");
footer.removeClass("notify");
change = true;
},
black: function() {
if (change) {
this.red();
}
}
};
};
return myFactory;
});
I ahve tried:
describe('<-- MyFactory Spec ------>', function () {
var myFactory, $compile, scope;
beforeEach(angular.mock.module('MyApp'));
beforeEach(inject(function(_myFactory_, _$compile_, _$rootScope_){
myFactory = _myFactory_;
$compile = _$compile_;
scope = _$rootScope_.$new();
}));
it('should open the menu', function(){
var header = angular.element("#app-header");
header = $compile(header)(scope);
scope.$digest();
myFactory.red();
scope.$apply();
expect(header).toHaveClass('alert-warning');
expect(change).toBeTruthy();
});
});
With the above, i get error:
TypeError: 'undefined' is not a function (evaluating 'expect(header).toHaveClass('alert-warning')')
I suspect you aren't pulling in jasmine-jquery matchers.
.toHaveClass(...)
Is not a standard Jasmine matcher, you need to add it with jasmine-jquery
Since I wrote firebase-factory separately from RecipeController, I have an error in my Test.
TypeError: Cannot read property '$loaded' of undefined.
$loaded is a method in firebase...
test.js
describe('RecipeController', function() {
beforeEach(module('leChef'));
var $controller;
beforeEach(inject(function(_$controller_){
$controller = _$controller_;
}));
describe("$scope.calculateAverage", function() {
it("calculates average correctly", function() {
var $scope = {};
var controller = $controller('RecipeController', { $scope: $scope });
$scope.calculateAverage();
expect(average).toBe(sum/(Recipes.reviews.length-1));
});
});
});
firebase-factory.js
app.factory("Recipes", ["$firebaseArray",
function($firebaseArray) {
var ref = new Firebase("https://fiery-inferno-8595.firebaseio.com/recipes/");
return $firebaseArray(ref);
}
]);
recipe-controller.js
app.controller("RecipeController", ["$scope", "toastr", "$location", "$routeParams", "$compile", "Recipes",
function($scope, toastr, $location, $routeParams, $compile, Recipes) {
$scope.recipes.$loaded().then(function(payload) {
$scope.recipe = payload.$getRecord($routeParams.id);
$scope.html = $scope.recipe.instructions;
if (typeof $scope.recipe.reviews === "undefined") {
$scope.recipe.reviews = [{}];
}
$scope.calculateAverage = function(AverageData){
var sum = 0;
if ($scope.recipe.reviews.length > 1) {
for(var i = 1; i < $scope.recipe.reviews.length; i++){
sum += parseInt($scope.recipe.reviews[i].stars, 10);
}
var average = sum/($scope.recipe.reviews.length-1);
var roundedAverage = Math.round(average);
return {
average: roundedAverage,
markedStars: new Array(roundedAverage)
};
} else {
return sum;
}
};
});
]);
In your RecipeController definition, you immediately call:
$scope.recipes.$loaded().then(function(payload) { ... }
...assuming that $scope.recipes is defined and has a property of $loaded -- which is not the case.
In your test:
describe("$scope.calculateAverage", function() {
it("calculates average correctly", function() {
var $scope = {};
var controller = $controller('RecipeController', { $scope: $scope });
$scope.calculateAverage();
expect(average).toBe(sum/(Recipes.reviews.length-1));
});
});
...you define scope as an empty object, then inject it into your controller.
Assuming you are using Jasmine as a test framework, you could create a spy like this:
var $scope = {
recipes: {
$loaded: function() { /* this is a mock function */ }
}
};
var deferred = $q.defer();
deferred.resolve({
/* this is the data you expect back from $scope.recipes.$loaded */
});
var promise = deferred.promise;
spyOn($scope.recipes, '$loaded').and.returnValue(promise);
This is just one of many ways you could stub out that function and control the data you get in your test. It assumes a basic understanding of the $q service and the Promise API.
Best Practices
It is best not to attach data to the $scope service. I would recommend reading up on the controllerAs syntax, if you're not familiar with it.
TL;DR: A controller is just a JavaScript "class", and the definition function is its constructor. Use var vm = this; and then attach variables to the instance reference vm (as in "view model", or whatever you want to call it) instead.
Rather than relying on $scope.recipes to have been defined elsewhere, you should explicitly define it in your controller. If recipes are defined in another controller, create a service that both controllers can share.
I'm an angular newbie and I'm writing an Ionic app.
I finished my app and am trying to refactor my controller avoiding code repetition.
I have this piece of code that manages my modal:
angular.module('starter')
.controller('NewsCtrl', function($scope, content, $cordovaSocialSharing, $timeout, $sce, $ionicModal){
$scope.news = content;
content.getList('comments').then(function (comments) {
$scope.comments = comments;
});
$scope.addComment = function() {
};
$scope.shareAnywhere = function() {
$cordovaSocialSharing.share("Guarda questo articolo pubblicato da DDay", "Ti stanno segnalando questo articolo", content.thumbnail, "http://blog.nraboy.com");
};
$ionicModal.fromTemplateUrl('templates/comments.html', {
scope: $scope,
animation: 'slide-in-up'
}).then(function(modal) {
$scope.modal = modal;
});
$scope.showComment = function() {
$scope.modal.show();
};
// Triggered in the login modal to close it
$scope.closeComment = function() {
$scope.modal.hide();
};
$scope.$on('modal.shown', function() {
var footerBar;
var scroller;
var txtInput;
$timeout(function() {
footerBar = document.body.querySelector('#commentView .bar-footer');
scroller = document.body.querySelector('#commentView .scroll-content');
txtInput = angular.element(footerBar.querySelector('textarea'));
}, 0);
$scope.$on('taResize', function(e, ta) {
if (!ta) return;
var taHeight = ta[0].offsetHeight;
if (!footerBar) return;
var newFooterHeight = taHeight + 10;
newFooterHeight = (newFooterHeight > 44) ? newFooterHeight : 44;
footerBar.style.height = newFooterHeight + 'px';
scroller.style.bottom = newFooterHeight + 'px';
});
});
});
I have added this same code in 6 controllers.
Is there a way to avoid the repetition?
Probably what you are looking for is an angular service. This component is a singleton object, that you inject in every controller you need to execute this code.
Angular Services
Regards,
Below is an example of a service I created to retrieve address data from a Json file. Here is the working Plunk. http://plnkr.co/edit/RRPv2p4ryQgDEcFqRHHz?p=preview
angular.module('myApp')
.factory('addressService', addressService);
addressService.$inject = ['$q', '$timeout', '$http'];
function addressService($q, $timeout, $http) {
var addresses = [];
//console.log("Number of table entries is: " + orders.length);
var promise = $http.get('address.data.json');
promise.then(function(response) {
addresses = response.data;
// console.log("Number of table entries is now: " + orders.length);
});
return {
GetAddresses: getAddresses
};
function getAddresses() {
return $q(function(resolve, reject) {
$timeout(function() {
resolve(addresses);
}, 2000);
});
}
}
Here's an example of how I added dependencies for it and another service to my controller (This is NOT the only way to do dependency injection, but is my favorite way as it is easier to read). I then called my addressService.GetAddresses() from within my controller.
var app = angular.module('myApp', ['smart-table']);
app.controller('TableController', TableController);
TableController.$inject = [ "orderService", "addressService"];
function TableController( orderService, addressService) {
addressService.GetAddresses()
.then(function(results) {
me.addresses = results;
// console.log(me.addresses.length + " addresses");
},
function(error) {})
.finally(function() {
me.loadingAddresses = false;
});
});}
I also had to include my .js tag in a script element on my index.html.
<script src="addressdata.service.js"></script>