How do I test my Angular scope functions in karma + jasmine? - javascript

I'm fairly new to Angular and very new to Jasmine testing. I have a function in my controller at pushes an object into an empty array (object taken from json data).
my controller with the functions pertaining to the cart:
$scope.cart = [];
$scope.addItemToCart = function(choc) {
var cartItem = readCartItem(choc.id);
if(cartItem == null) {
//if item doesn't exist, add to cart array
$scope.cart.push({type: choc.type, id: choc.id, price: choc.price, quantity: 1})
} else {
//increase quantity
cartItem.quantity++;
}
}
$scope.cartTotal = function() {
var sum = 0;
$scope.cart.forEach(function(item) {
sum += item.price * item.quantity;
});
return sum;
}
$scope.getTotalQuantity = function() {
var totalItems = 0;
$scope.cart.forEach(function(item) {
totalItems += item.quantity;
});
return totalItems;
}
$scope.clearCart = function() {
$scope.cart.length = 0;
}
$scope.removeItem = function(choc) {
$scope.cart.splice(choc,1);
}
function readCartItem(id) {
//iterate thru cart and read ID
for(var i=0; i<$scope.cart.length; i++) {
if($scope.cart[i].id === id) {
return $scope.cart[i]
}
}
return null;
}
My test:
describe('Controller: ChocoListCtrl', function () {
beforeEach(module('App'));
var scope, ctrl, json;
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
// ChocoListCtrl = $controller('ChocoListCtrl', {});
ctrl = $controller("ChocoListCtrl", { $scope:scope })
}));
it('should be defined', function (){
expect(ctrl).toBeDefined();
});
it('should have an empty cart', function(){
expect(scope.cart.length).toBeLessThan(1);
});
describe('cart functions', function(){
beforeEach(function(){
scope.addItemToCart();
})
it('should add objects into the cart', function(){
expect(scope.cart.length).toBeGreaterThan(0);
})
});
The error I come back with when running the test:
TypeError: undefined is not an object (evaluating 'choc.id')
I thought I was pushing an object into the array? Am I missing something? Should I include the JSON file if it helps?
Any guidance would help. Thank you!

You're not passing in a parameter to $scope.addItemToCart. So when it tries to read choc it can't because it's undefined.
This line is causing the error:
beforeEach(function(){
scope.addItemToCart(); // No parameter being passed in
})

Related

$watch function is not getting triggered

I have a service which returns reponse. In response I have count of Users. I have a var userCount and I have a watch() on this userCount var.
var userCount=null;
var eventPromise = userSer.load(query, deviceType,$scope.duration);
eventPromise.then(function(response) {
console.log(response)
var dataLength = response.users.length;
$scope.numberOfRecords = dataLength;
if(dataLength > 0){
userCount = response.beaconCount;
setUserCount(userCount);
}
var setUserCount=function(data){
userCount=data;
};
var getUserCount=function(){
return userCount;
}
// This $watch function is not getting trigger as we are changing value of userCount from null to response.userCount.
$scope.$watch(userCount, function(newVal){
alert("M in watch func");
$scope.gridOptions.columnDefs[0].displayName = 'Beacon(' + getUserCount() + ')';
$scope.gridApi.core.notifyDataChange(uiGridConstants.dataChange.COLUMN);
})
You have messed up the usage of $scope.$watch, the correct usage of $scope.$watch is as below, refer docs here:
Usage1: watching changes of variable belongs to $scope.
$scope.userCount = null;
$scope.$watch("userCount", function() { ... });
Usage2: watching changes of variable not belongs to $scope.
var userCount = null;
$scope.$watch(function() { return userCount; }, function() { ... });
refer the below example.
angular.module("app", [])
.controller("myCtrl", function($scope, $timeout) {
$scope.data = null;
var data2 = null;
$scope.$watch("data", function() {
console.log("data change detected.");
});
$scope.$watch(function() { return data2; }, function() {
console.log("data2 change detected.");
});
$timeout(function() {
$scope.data = {id: 1};
data2 = {id: 2};
}, 2000);
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.min.js"></script>
<div ng-app="app" ng-controller="myCtrl">
</div>
You cannot $watch any variable like this. Assign the userCount variable to $scope and it will work.
$scope.userCount = null;
userSer.load(query, deviceType, $scope.duration).then(function (response) {
console.log(response);
var dataLength = response.users.length;
$scope.numberOfRecords = dataLength;
if (dataLength > 0) {
$scope.userCount = response.beaconCount;
}
});

Controller isn't counted as a function

I have many different controllers throughout this project and all of them are declared the same way. This one now isn't getting called/giving an error and I have no clue why. I've looked through it and it all looks right to me.
I think it's probably some syntax error I'm not seeing. If its something else please tell me. I'm trying to learn angular and everything helps. Also if you need anything else just tell me.
I've made sure its not that the app.js name got changed and been looking for missing syntax but can't find anything.
https://docs.angularjs.org/error/ng/areq?p0=companyDepartmentController&p1=not%20a%20function,%20got%20undefined
company-department-controller.js
app.controller('companyDepartmentController', ['$scope', '$timeout', 'companyService', function ($scope, $timeout, companyService) {
/**
* Create/Manage Company Departments & Shifts
*
*/
// INITIALIZE VARIABLES *********************************************************************************
var vm = this;
vm.Departments = [];
vm.activeDepartment = {}
vm.departmentBeforeEdit = {};
vm.activeShift = {};
vm.OffsetString = "";
vm.SaveDepartmentSuccessMessage = null;
vm.SaveDepartmentErrorMessage = null;
// STARTUP **********************************************************************************************
(vm.GetDepartments = function () {
companyService.GetDepartmentsWithShiftInformation().success(function (data) {
console.log('hi');
for (i = 0; i < data.length; i++) {
console.log(data[i])
}
vm.Departments = data;
// for now we are limiting this to 1
vm.activeDepartment = vm.Departments[0];
vm.setTimeZoneOffsets(vm.activeDepartment);
});
})();
// move to global location? handle this better?
(vm.findLocalOffsetString = function () {
console.log('hi1');
vm.OffsetString = moment(new Date()).format('ZZ');
})();
// $BROADCAST/$ON EVENTS ********************************************************************************
// EVENTS ***********************************************************************************************
vm.saveDepartment = function (department) {
// new
if (department.DepartmentID === 0 || typeof department.DepartmentID === 'undefined') {
}
// update
else {
companyService.UpdateDepartmentHeader(department).success(function (data) {
vm.SaveDepartmentSuccessMessage = "Saved!";
resetDepartmentMessage();
department.InEdit = false
});
}
};
vm.editDepartment = function (department) {
vm.activeDepartment = department;
vm.departmentBeforeEdit = angular.copy(vm.activeDepartment);
vm.activeDepartment.InEdit = true;
};
vm.cancelDepartmentEdit = function (department) {
for (var i = 0; i < vm.Departments.length; i++) {
if (department.DepartmentID === vm.Departments[i].DepartmentID) {
vm.Departments[i] = vm.departmentBeforeEdit;
vm.departmentBeforeEdit = {};
vm.activeDepartment = vm.Departments[i];
break;
};
};
};
vm.addShift = function () {
if (!vm.activeDepartment) return;
vm.activeShift = {
DepartmentID: vm.activeDepartment.DepartmentID,
StartTime: new Date(),
LocalStartTime: new Date(new Date() + vm.OffsetString)
};
vm.activeShift.StartTime.setSeconds(0);
vm.activeShift.LocalStartTime.setSeconds(0);
};
vm.deleteShift = function (shift) {
if (!shift) return;
if (confirm("Are you sure you want to delete the shift: " + shift.Name + "?")) {
companyService.DeleteShift(shift).success(function () {
angular.forEach(vm.activeDepartment.Shifts, function (c, i) {
if (c.ShiftID === shift.ShiftID) {
vm.activeDepartment.Shifts.splice(i, 1);
};
});
});
};
};
vm.setTimeZoneOffsets = function (department) {
if (!department || !department.Shifts || department.Shifts.length === 0) return;
for (var i = 0; i < department.Shifts.length; i++) {
department.Shifts[i].LocalStartTime = new Date(department.Shifts[i].StartTime + vm.OffsetString);
department.Shifts[i].EndTime = moment(department.Shifts[i].StartTime).add(department.Shifts[i].Duration, 'hours').toDate()
};
};
var fixTimezoneOnSave = function (shift) {
shift.StartTime = new Date(shift.LocalStartTime).toLocaleString();
};
vm.setActiveShift = function (shift) {
if (!shift) return;
vm.activeShift = angular.copy(shift);
};
vm.saveShift = function (shift) {
fixTimezoneOnSave(shift);
// new shift
if (shift.ShiftID === 0 || typeof shift.ShiftID === 'undefined') {
companyService.AddShift(shift).success(function (data) {
shift.ShiftID = data;
vm.SaveDepartmentSuccessMessage = "Saved!";
resetDepartmentMessage();
getUpdatedShiftsAndInfo();
}).error(function (e) {
vm.SaveDepartmentErrorMessage = e.error;
resetDepartmentMessage();
});
}
// updating existing
else {
companyService.UpdateShift(shift).success(function (data) {
vm.SaveDepartmentSuccessMessage = "Saved!";
resetDepartmentMessage();
getUpdatedShiftsAndInfo();
}).error(function (e) {
vm.SaveDepartmentErrorMessage = e.error;
resetDepartmentMessage();
});
}
}
var getUpdatedShiftsAndInfo = function () {
companyService.DepartmentAndShiftInformation(vm.activeDepartment.DepartmentID).success(function (data) {
vm.activeDepartment.DepartmentShiftInformation = data.DepartmentShiftInformation;
vm.activeDepartment.Shifts = data.Shifts;
vm.setTimeZoneOffsets(vm.activeDepartment);
});
};
var resetDepartmentMessage = function () {
// clear error/success message if they have values still
if (vm.SaveDepartmentSuccessMessage != null) {
$timeout(function () { vm.SaveDepartmentSuccessMessage = null; }, 2000);
}
if (vm.SaveDepartmentErrorMessage != null) {
$timeout(function () { vm.SaveDepartmentErrorMessage = null; }, 2000);
}
};
// create controller object in console if we have logging turned on
if (spectrum.LoggingEnabled) {
spectrum.logController(vm);
};
}]);
_CompanyDepartment.cshtml
<div class="container-fluid" data-ng-controller="companyDepartmentController as cd">
</div>
#section scripts {
#Scripts.Render("~/bundles/companyDepartments")
#Scripts.Render("~/bundles/jquery")
#Scripts.Render("~/bundles/angularjs")
}
app.js
var app = angular.module('app', ['angularFileUpload', 'ngSanitize', 'ui.mask', 'ui.select', 'ui.bootstrap', 'ui.bootstrap.tpls', 'angular.filter', 'smart-table', 'colorpicker.module'])
.config(function ($httpProvider) {
//make delete type json to facilitate passing object
//to our generic method.
$httpProvider.defaults.headers["delete"] = {
'Content-Type': 'application/json;charset=utf-8'
};
});
Outside of a naming issue with the controller(which I can't see), I would imagine your issue dealing with the company-department-controller.js not being loaded.
In setting up your angular project, I would suggest that you follow this angular styleguide. It has been very helpful to me in creating a well structured project.

Reusing recurrent function in AngularJS

I have a function that keeps repeating itself in my controller.
It looks like this:
//FUNCTION 1
$scope.selectedage = [];
$scope.pushage = function (age) {
age.chosen = true;
$scope.selectedage.push(age);
console.log($scope.selectedage);
};
$scope.unpushage = function (age) {
age.chosen = false;
var index=$scope.selectedage.indexOf(age)
$scope.selectedage.splice(index,1);
console.log($scope.selectedage);
}
//FUNCTION 2
$scope.selectedgender = [];
$scope.pushgender = function (gender) {
gender.chosen = true;
$scope.selectedgender.push(gender);
console.log($scope.selectedgender);
};
$scope.unpushgender = function (gender) {
gender.chosen = false;
var index=$scope.selectedgender.indexOf(gender)
$scope.selectedgender.splice(index,1);
console.log($scope.selectedgender);
}
I have it like 8 times for 8 different arrays.
Is there any way to write it once and reuse it just changing some values?
You can make a generic function that accepts a value (container) where it needs to write the "value". Like:
$scope.push = function(container, value){
value.chosen = true;
container.push(value);
console.log(container);
}
$scope.unpush = function(container, value){
value.chosen = false;
var index = container.indexOf(value)
container.splice(index, 1);
console.log(container);
}
//sample
$scope.push($scope.selectedage, 10);
$scope.push($scope.selectedgender, "Male");
function togglePushStatusOfItem(item, itemKey, chosenStatus){
item.status = chosenStatus;
if(chosenStatus == true){
$scope[itemKey].push(item);
} else {
var index=$scope[itemKey].indexOf(item)
$scope[itemKey].splice(index,1);
}
console.log($scope[itemKey]);
}
togglePushStatusOfItem(user, 'selectedAge',true);
refactoring code to be reused in service
(function() {
'use strict';
angular
.module('myApp')
.factory('ItemFactory', ItemFactory);
ItemFactory.$inject = [];
/* #ngInject */
function ItemFactory() {
var service = {
toggleItemStatus: toggleItemStatus
};
return service;
////////////////
/*
itemContainer - equivalent to scope
item - item to replace or push
itemKey - itemKey
chosenStatus - true to push and false to remove
*/
function toggleItemStatus(itemContainer, item, itemKey, chosenStatus) {
item.status = chosenStatus;
if (chosenStatus == true) {
itemContainer[itemKey].push(item);
} else {
var index = $scope[itemKey].indexOf(item)
itemContainer[itemKey].splice(index, 1);
}
console.log(itemContainer[itemKey]);
}
}
})();
in your controller, you may use it like this
ItemFactory.toggleItemStatus($scope, item, 'selectedAge', true);// to push item
ItemFactory.toggleItemStatus($scope, item, 'selectedAge', false);// to remove item
The only difference I made is that I used the same function to push and unpush item. I hope this doesn't confuse you.

Optimization of angularjs function to load images on the page

I have two angularjs function in my dataContext object that receive data from an webapi as you can see below
(function () {
'use strict';
var serviceId = 'datacontext';
angular.module('app').factory(serviceId,
['common', datacontext]);
function datacontext(common) {
var service = {
getAllTagsByHttp: getAllTagsByHttp,
getBlogPostsByHttp: getBlogPostsByHttp,
getAllUserPicByHttp: getAllUserPicByHttp
};
return service;
function getAllTagsByHttp() {
return common.$http.get('/api/TagApi/alltags');
}
function getBlogPostsByHttp() {
return common.$http.get('/api/BlogPostApi/AllBlogPosts');
}
function getAllUserPicByHttp() {
return common.$http.get('/api/BlogPostApi/AllUserPic');
}
}})();
I have an angularjs controller that call these api functions from the datacontext to receive information to display back to the user. The getblogPost function is called first because the next function need's the data from this to operate on.the second function does a comparison on the first function's data to display back the img source to the user. The problem i am having is that it takes quite a few seconds for the second function to receive the data and an complete its operation. The amount of data being send back is a few records.Is there any improvements that can be made to controller or data Context object to reduce the time or improve the code?
(function () {
'use strict';
var controllerId = 'search';
angular
.module('app')
.controller(controllerId, ['common','datacontext', search]);
function search(common, datacontext) {
var getLogFn = common.logger.getLogFn;
var log = getLogFn(controllerId);
var vm = this;
vm.title = 'search';
vm.picData = [];
activate();
function activate() {
var promises = [getBlogPosts(), getUserPicData(), getUserPicUrl()];
common.activateController(promises, controllerId)
.then(function () { log('Search View'); });
}
function getBlogPosts() {
datacontext.getBlogPostsByHttp().then(function (response) {
vm.data = response.data;
});
}
function getUserPicData() {
datacontext.getAllUserPicByHttp().then(function (response) {
vm.picData = response.data;
for (var i = 0; vm.data.length > i; i++) {
for (var x = 0; vm.picData.length > x; x++) {
if (vm.data[i].UserName == vm.picData[x].UserName) {
vm.data[i].PictureUrl = vm.picData[x].PictureUrl;
// console.log(vm.data[i].PictureUrl);
break;
}
}
}
});
}
function getUserPicUrl(username) {
getUserPicData();
for (var i = 0; vm.picData.length < i; i++) {
if (vm.picData[i].UserName === username) {
return vm.picData[i].PictureUrl;
}
}
return "not found";
}
}})();

How to pass functions from factory into controller angularJS

Right now I have this JS bin: http://jsbin.com/uhabed/64/ in which you can hopefully see the infite scroll loading of more images. These images are added when the page bottom is reached by the scroll bar on line 13 of the javascript:
angular.module('infinitescroll', []).directive('onScrolled', function () {
return function (scope, elm, attr) {
var el = elm[0];
elm.bind('scroll', function () {
if (el.scrollTop + el.offsetHeight >= el.scrollHeight) {
scope.$apply(attr.onScrolled);
}
});
};
}).controller("scrollCtrl", function($scope, getStuff){
$scope.data = getStuff;
$scope.loaddata = function(){
var length = $scope.data.length;
for(var i = length; i < length + 10; i ++){
$scope.data.push(i);
}
};
$scope.loaddata();
}).factory('getStuff',function($http) {
var images_list = ["www.google.com","www.facebook.com","www.supercuber.com","www.snappiesticker.com"];
images_list.addStuff = function(){ $http.get("http://jsbin.com/yehag/2").success(function(data){
var returned_list = JSON.parse(data.javascript);
console.log(returned_list);
for (var i=0;i<8;i++){
images_list.push(returned_list[i].name);
}
});
};
console.log(images_list);
return images_list;
});
I want to replace line 13 with $scope.loaddata = images_list.addStuff(); from the factory below. basically to use the $http function and add the data from that instead. images_list is already being returned properly seeing as the first 4 items in the output are the ones defined in the factory on line 21. However, the optomistic $scope.loaddata = images_list.addStuff(); doesn't seem to be working.
How can I pass this function up into the $scope.loaddata?
images_list is an array. You can't arbitrarily assign a property to it like images_list.addStuff
Create an object and return that object from the factory
factory('getStuff',function($http) {
var images_list = ["www.google.com","www.facebook.com","www.supercuber.com","www.snappiesticker.com"];
var addStuff = function(){....};
return{
images_list: images_list,
addStuff: addStuff
}
});
Then in controller:
$scope.data = getStuff.images_list;
$scope.loaddata = getStuff.addStuff
This is not a clean way of having an array-like object that has additional properties on it, however the above answer that you 'cannot add functions onto an array' is incorrect. While creating array-like objects is kind of messy and should be avoided where possible. If you feel it is absolutely necessary, I would handle it like this (this is similar to how jQuery does it.
function ImagesList($http) {
this.push.apply(this, [
"www.google.com",
"www.facebook.com",
"www.supercuber.com",
"www.snappiesticker.com"
]);
this._$http = $http;
}
ImagesList.prototype = {
push: [].push,
splice: [].splice,
pop: [].pop,
indexOf: [].indexOf,
addStuff: function () {
this._$http.get("http://jsbin.com/yehag/2").then(function(data){
var returnedList = angular.toJson(data.javascript);
for (var i=0; i<8; i++) {
this.push(returnedList[i].name);
}
}.bind(this));
}
};
angular
.module('infinitescroll', [])
.service('imageList', ImagesList);
.directive('onScrolled', function () {
return {
scope: {
onScrolled: '&'
},
link: function (scope, elm, attr) {
var el = elm[0];
// Original implementation will end up causing memory leak because
// the handler is never destroyed. Use as follows
elm.on('scroll.iScroll', function () {
if (el.scrollTop + el.offsetHeight >= el.scrollHeight) {
scope.$apply(attr.onScrolled);
}
}).on('$destroy', function () {
elm.off('.iScroll');
});
}
};
}).controller("scrollCtrl", function($scope, imageList){
$scope.data = imageList;
$scope.loaddata = function(){
var length = $scope.data.length;
for(var i = length; i < length + 10; i++){
$scope.data.push(i);
}
};
$scope.loaddata();
})

Categories

Resources