Angular Unit testing with Jasmine / Karma - javascript

I'm trying to write a unit test script for custom directive with jasmine and karma.
Current application is Angular 1.28; Issue is above code not running properly. Can anyone help/guide me that where i'm making mistake; or how to write unit test case for custom directives. Any suggestions or help will be greatly appreciated, thanks.
Directive as below:
angular.module('phone.filter', [])
.directive('phoneField', ['$filter', function ($filter) {
var phoneFilter = $filter('phoneFilter'),
reversePhone = $filter('reversePhone');
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ngModel) {
var formatter = value => phoneFilter(value),
parser = function (value) {
var formatted = reversePhone(value);
element.val(phoneFilter(formatted));
//prevents a phone number from being all zeros
ngModel.$setValidity('phone', /^(?!0{10})(\d{10})?$/.test(value.replace(/\D/g, '')))
return formatted;
}
ngModel.$formatters.push(formatter);
return ngModel.$parsers.unshift(parser);
}
};
}]).filter('phoneFilter', function () {
return function (value) {
var len, val;
if (value) {
val = value.toString().replace(/\D/g, '');
len = val.length;
if (len < 4) {
return val;
} else if ((3 < len && len < 7)) {
return (val.substr(0, 3)) + '-' + (val.substr(3));
} else if (len >= 7) {
return (val.substr(0, 3)) + '-' + (val.substr(3, 3)) + '-' + (val.substr(6));
}
}
return value;
};
}).filter('reversePhone', function () {
return (value = '') => value.replace(/\D/g, '').substr(0)
})
Test scripts as below:
import chai, { expect } from 'chai'
// import { createElement } from '../helpers/directive-testing-helper'
/* import { stub } from 'sinon'
chai.use(require('sinon-chai'))
*/
const module = angular.mock.module
describe('Phone Field validation directive (phoneField)', () => {
let $rootScope,
$compile;
beforeEach(() => module('pru.com.components.phone.filter'))
beforeEach(inject((_$rootScope_, _$compile_) => {
$rootScope = _$rootScope_;
$compile = _$compile_;
}))
it('should not have all zeros', () => {
$rootScope.phone = "0002345678"
let element = $compile('<input type="text" name="phoneNumber" phone-field="" value="0002345678" ng-model="">')($rootScope);
$rootScope.$digest();
chai.expect(new RegExp(/phone-field/)).match(element.html()).to.be.true;
});
})

Related

AngularJS: I can't understand a custom directive

Can someone help me to understand this code? I've found this code in a project and I need a directive that allows only numeric input in text areas, can this help me?
controllerManager.directive('chars', function () {
'use strict';
return {
require: 'ngModel',
restrict: 'A',
link: function ($scope, $elem, attrs, ctrl) {
var regReplace,
preset = {
'specialChars': "^\x20-\x22\x27-\x3F\x41-\x59\x61-\x7B\x7D",
'integer': "0-9"
},
filter = preset[attrs.chars] || attrs.chars;
$elem.on('input', function () {
regReplace = new RegExp('[' + filter + ']', 'ig');
ctrl.$setViewValue($elem.val().replace(regReplace, ''));
ctrl.$render();
var max = $elem.attr('maxLength');
var len = $elem.val().length;
document.getElementById($elem.attr('id')+"Count").innerHTML = max - len;
});
}
};
});
Use below directive. It will only allow integer values to be entered and will discard any other character.
app.directive('integerOnly', ['$compile', function ($compile) {
return {
restrict: 'A',
require: "ngModel",
scope: {
ngModel: '='
},
link: function (scope, element, attrs, ngModelCtrl) {
var oldValue = null;
element.addClass('text-right');
element.bind('focus', function () {
return scope.$apply(function () {
return oldValue = element.val();
});
});
return ngModelCtrl.$parsers.unshift(function (inputValue) {
var val = inputValue ? inputValue.toString().replace(/[^0-9]/g, '') : "";
while (val < "1" && val != val.replace(/[^1-9]/, '')) {
val = val.replace(/[^1-9]/, '');
}
if (val !== inputValue) {
ngModelCtrl.$setViewValue(val);
ngModelCtrl.$render();
}
return val;
});
}
};
}]);
I resolved with this change:
controllerManager.directive('chars', function () {
'use strict';
return {
require: 'ngModel',
restrict: 'A',
link: function ($scope, $elem, attrs, ctrl) {
var regReplace,
preset = {
'specialChars': "^\x20-\x22\x27-\x3F\x41-\x59\x61-\x7B\x7D",
'integer': "^0-9"
},
filter = preset[attrs.chars] || attrs.chars;
$elem.on('input', function () {
regReplace = new RegExp('[' + filter + ']', 'ig');
ctrl.$setViewValue($elem.val().replace(regReplace, ''));
ctrl.$render();
var max = $elem.attr('maxLength');
var len = $elem.val().length;
document.getElementById($elem.attr('id')+"Count").innerHTML = max - len;
});
}
};
});
So now this work:
<input type="text" chars="integer"/>

how to write a unit test case for the following angular function

how to write a unit test case for the following angular function. I'm new to karma and jasmine. function with rootscope and also have to test window.open inside if statement.
$rootScope.getStandardMapPDF = function (mt, g, u, m)
{
var menuTitle = mt.trim();
var grade = g.trim();
var unit = u.trim();
var module = m.trim();
/*---->Getting the Grade, Unit and Module wise Help files <-------*/
if ($.isEmptyObject($rootScope.StandardMapFiles)) {
$rootScope.StandardMapFiles = DataProvider.StandardHelpMaster;
}
var obj = $rootScope.StandardMapFiles;
for (var i = 0; i < obj.length; i++) {
if (obj[i].Grade.toLowerCase().indexOf(grade.toLowerCase()) != -1 && obj[i].Unit.toLowerCase().indexOf(unit.toLowerCase()) != -1 && obj[i].Module.toLowerCase().indexOf(module.toLowerCase()) != -1 && obj[i].MenuTitle.toLowerCase() == menuTitle.toLowerCase()) {
if (obj[i].FileType.toLowerCase() == 'pdf') {
var path = 'Resources/StandardMappings/' + obj[i].FileName.trim() + '.pdf';
//var path = '/fs/oleshare/ole-mygen/StandardMappings/' + obj[i].FileName.trim() + '.pdf';
$window.open(path, '_blank');
}
else if (obj[i].FileType.toLowerCase() == 'video') {
var path = 'Resources/Video/' + obj[i].FileName.split('.')[0].trim() + '.htm';
$window.open(path, '_blank');
}
}
}
};
Here's a basic outline of how you write a test:
(function() {
describe('your.controller.name.js', function() {
var $rootScope, $window;
// Init
beforeEach(module('your-app-name'));
beforeEach(inject(function(_$rootScope_,_$window_) {
$rootScope = _$rootScope_;
$window = _$window_;
}));
// Spies
beforeEach(function() {
spyOn($window,'open');
});
it('should be defined', function() {
expect($rootScope.getStandardMapPDF).toBeDefined();
});
describe('$rootScope.getStandardMapPDF', function() {
beforeEach(function() {
$rootScope.getStandardMapPDF()
});
it('should call $window.open', function() {
expect($window.open).toHaveBeenCalled();
});
});
}());
Why are you attaching a function to the $rootScope anyway?

Angular filter with typescript is not working after minification

I wrote an angular filter with typescript which works fine until I minify the source code.
Here is the filter:
module App.Test {
export interface IGroupingFilter extends ng.IFilterService {
(name:"grouping-filter"): (collection:any[]) => collection:any[];
}
class GroupingFilter {
static $inject:string[] = ["underscore"];
static ConvertDateTime(item:any):number {
var time = "" + item.time;
var newTime = (time.length == 3) ? "0" + time : time;
return +(item.pickupDate.replace(/\-/g, '') + newTime);
}
public static Factory(underscore:UnderscoreStatic) {
return underscore.memoize((collection:any[]) => {
var groupKey = "id";
var group:any = underscore.groupBy(collection, (item:any) => {
return item[groupKey];
});
var grpArray = [];
angular.forEach(group, (item) => {
grpArray.push({
"groupKey": item[0][groupKey],
"items": item
});
});
var grpArraySorted = underscore.sortBy(grpArray, (grpObj:any) => {
var min:any = underscore.min(grpObj.items, (item:any) => {
return GroupingFilter.ConvertDateTime(item);
});
return GroupingFilter.ConvertDateTime(min);
});
return grpArraySorted;
});
}
}
angular.module("app").filter("groupingFilter", GroupingFilter.Factory);
}
Here is the minified version:
var App;
!function (t) {
var e;
!function (t) {
var e = function () {
function t() {
}
return t.ConvertDateTime = function (t) {
var e = "" + t.time, r = 3 == e.length ? "0" + e : e;
return +(t.pickupDate.replace(/\-/g, "") + r)
}, t.Factory = function (e) {
return e.memoize(function (r) {
var n = "id", i = e.groupBy(r, function (t) {
return t[n]
}), o = [];
angular.forEach(i, function (t) {
o.push({groupKey: t[0][n], items: t})
});
var a = e.sortBy(o, function (r) {
var n = e.min(r.items, function (e) {
return t.ConvertDateTime(e)
});
return t.ConvertDateTime(n)
});
return a
})
}, t.$inject = ["underscore"], t
}();
angular.module("app").filter("groupingFilter", e.Factory)
}(e = t.Test || (t.Test = {}))
}(App || (App = {}));
Here is the angular error message
Error: [$injector:unpr] Unknown provider: eProvider <- e <-
groupingFilterFilter
Many thanks
The reason it does not work when minified is that you inject "underscore" into the FooFilter class not the actual filter, which is the result of FooFilter.Factory. To create such a simple filter you don't really need a class, just pass a simple function.
angular.module('app').filter('fooFilter', fooFilter);
fooFilter.$inject = ['underscore'];
function fooFilter(underscore) {
return underscore.memoize((collection:any[]) => {
return underscore.shuffle(collection);
});
}
If you really want to write the filter factory function as a static class method, you could use the array syntax like this:
angular.module("app")
.filter("groupingFilter", ['underscore', GroupingFilter.Factory]);
Remove the $inject array from your class in this case.

Pass scope and parameters to directive - AngularJS

I'm trying to set a class in a ng-repeat with a directive.
I need to pass a parameter to this directive: wineQty
if I use scope: { wineQty: '=' } this works however $scope.bullet1Color is undefined in my html and thus doesn't affect the class that I want.
If I use scope: '#' I get the correct class however I can't specify wineQty
Is there a way to combine theses two syntaxes? so that I can access the scope and pass a paramater?
I've tried { wineQty: '#' } but with no luck sadly.
Here's my directive
angular.module('myApp').directive('wineQtyBullets', function () {
return {
restrict: 'A',
scope: { wineQty: '=', totalBullets: '=', numRedBullets: '=', numOrangeBullets: '#', bullet1Color: '#' },
link: function (scope, element, attrs) {
// defaults
var defaultNumRedBullets = 1;
var defaultNumOrangeBullets = 2;
var defaultTotalBullets = 12;
var defaultWineQty = 0;
// set values from attributes
var numRedBullets = scope.numRedBullets ? scope.numRedBullets : defaultNumRedBullets;
var numOrangeBullets = scope.numOrangeBullets ? scope.numOrangeBullets : defaultNumOrangeBullets;
var totalBullets = scope.totalBullets ? scope.totalBullets : defaultTotalBullets;
var wineQty = scope.wineQty ? scope.wineQty : defaultWineQty;
for (var currentBullet = 1; currentBullet <= totalBullets; currentBullet++) {
var bulletColorClass = 'bg-grey';
if (currentBullet <= wineQty) {
if (currentBullet <= numRedBullets) {
bulletColorClass = 'bg-red';
}
else if (currentBullet <= (numOrangeBullets + numRedBullets)) {
bulletColorClass = 'bg-orange';
}
else {
bulletColorClass = 'bg-green';
}
}
scope["bullet" + currentBullet + "Color"] = bulletColorClass;
}
console.log(scope.bullet1Color);
}
};
}
);
Here's my html
<div class="bullets pull-left ml15 mt6" wine-qty="wine.owned_qty" wine-qty-bullets>
<ul>
<li class="bullet"
ng-class="bullet1Color"></li>
</ul>
</div>
I managed to solve the problem, by changing scope to true and accessing the parameters through attrs
If this can help anyone here's the directive:
angular.module('myApp').directive('wineQtyBullets', function () {
return {
restrict: 'A',
scope: true,
link: function (scope, element, attrs) {
var options = { };
angular.forEach([
'numRedBullets',
'numOrangeBullets',
'totalBullets',
'wineQty'
], function (key) {
if (angular.isDefined(attrs[key]))
options[key] = attrs[key];
});
// defaults
var defaultNumRedBullets = 1;
var defaultNumOrangeBullets = 1;
var defaultTotalBullets = 12;
var defaultWineQty = 0;
// set values from attributes
var numRedBullets = parseInt(options.numRedBullets) ? parseInt(options.numRedBullets) : defaultNumRedBullets;
var numOrangeBullets = parseInt(options.numOrangeBullets) ? parseInt(options.numOrangeBullets) : defaultNumOrangeBullets;
var totalBullets = parseInt(options.totalBullets) ? parseInt(options.totalBullets) : defaultTotalBullets;
var wineQty = parseInt(options.wineQty) ? parseInt(options.wineQty) : defaultWineQty;
for (var currentBullet = 1; currentBullet <= totalBullets; currentBullet++) {
var bulletColorClass = 'bg-grey';
if (currentBullet <= wineQty) {
if (wineQty <= numRedBullets) {
bulletColorClass = 'bg-red';
}
else if (wineQty <= (numOrangeBullets + numRedBullets)) {
bulletColorClass = 'bg-orange';
}
else {
bulletColorClass = 'bg-green';
}
}
scope["bullet" + currentBullet + "Color"] = bulletColorClass;
}
}
};
});
Using "=" means 2-way data binding, and it's definitely fine.
The problem might be that your link function are executed only once at the very beginning, when it's possible that the values of your attributes are not yet assigned (may be caused by some AJAX calls).
I would suggest that you wrap all your link function into a scope.$watch function. Like:
link: function (scope, element, attrs) {
scope.$watch(function() {
return {
wineQty: scope.wineQty,
totalBullets: scope.totalBullets,
numRedBullets: scope.numRedBullets,
numOrangeBullets: scope.numOrangeBullets,
bullet1Color: scope.bullet1Color
}
}, function() {
// Your original code here.
})
}
Then your final result will automatically update if your directive got new attribute values.

AngularJS - Blur + Changed?

What is the easiest way to combine ng-changed and ng-blur?
I've found this post: How to let ng-model not update immediately?
However, this does no longer work in angluar 1.2+
Is there any way to achieve the same behavior?
I guess I have to store a copy of the old value myself and compare the new value to that on blur if I try to do the same, or is there any easier way ?
Use ng-model options.
Like this, ng-change will only trigger when the input is blurred.
<input type="text"
ng-model="a.b"
ng-model-options="{updateOn: 'blur'}"
ng-change="onchange()"/>
This does what I want.
It stores the value on focus, and compares it to the new value on blur, if changed, it triggers the expression in the attribute.
app.directive('changeOnBlur', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elm, attrs, ngModelCtrl) {
if (attrs.type === 'radio' || attrs.type === 'checkbox')
return;
var expressionToCall = attrs.changeOnBlur;
var oldValue = null;
elm.bind('focus',function() {
scope.$apply(function() {
oldValue = elm.val();
console.log(oldValue);
});
})
elm.bind('blur', function() {
scope.$apply(function() {
var newValue = elm.val();
console.log(newValue);
if (newValue !== oldValue){
scope.$eval(expressionToCall);
}
//alert('changed ' + oldValue);
});
});
}
};
});
usage:
<input ng-model="foo" change-on-blur="someFunc()" />
How about this solution. Works for me:
<input ng-init="oldValue = value" ng-model="value"
ng-blur="oldValue != value && callYourFunc(foo)">
I am using AngularJs 1.2.x and stumbled upon the ng-change issue of firing on each change. ng-blur can be used but it fires even though there is no change in the value. So both cannot be used efficiently.
With Angularjs 1.3.x, things are easier using ng-model-options like below
to invoke change function "onBlur"
ng-change="ctrl.onchange()" ng-model-options="{updateOn: 'blur'}"
And
to delay invocation of change function by 500ms
ng-change="ctrl.onchange()" ng-model-options='{ debounce: 500 }'"
Now coming to back to the question of getting such things with AngularJs 1.2.x
to invoke change function "onBlur"
html
<input type="text" ng-model="ctrl.a.c" sd-change-on-blur="ctrl.onchange()" />
or
<input type="text" ng-model="ctrl.a.c" sd-change-on-blur="ctrl.onchange(ctrl.a.c)" />
JS
app.directive('sdChangeOnBlur', function() {
return {
restrict: 'A',
scope: {
sdChangeOnBlur: '&'
},
link: function(scope, elm, attrs) {
if (attrs.type === 'radio' || attrs.type === 'checkbox')
return;
var parameters = getParameters(attrs.sdChangeOnBlur);
var oldValue = null;
elm.bind('focus', function() {
scope.$apply(function() {
oldValue = elm.val();
});
})
elm.bind('blur', function() {
scope.$apply(function() {
if (elm.val() != oldValue) {
var params = {};
if (parameters && parameters.length > 0) {
for (var n = 0; n < parameters.length; n++) {
params[parameters[n]] = scope.$parent.$eval(parameters[n]);
}
} else {
params = null;
}
if (params == null) {
scope.sdChangeOnBlur();
} else {
scope.sdChangeOnBlur(params)
}
}
});
});
}
};
});
function getParameters(functionStr) {
var paramStr = functionStr.slice(functionStr.indexOf('(') + 1, functionStr.indexOf(')'));
var params;
if (paramStr) {
params = paramStr.split(",");
}
var paramsT = [];
for (var n = 0; params && n < params.length; n++) {
paramsT.push(params[n].trim());
}
return paramsT;
}
to delay invocation of change function by 500ms
html
<input type="text" ng-model="name" sd-change="onChange(name)" sd-change-delay="300"/>
OR
<input type="text" ng-model="name" sd-change="onChange()" sd-change-delay="300"/>
JS
app.directive('sdChange', ['$timeout',
function($timeout) {
return {
restrict: 'A',
scope: {
sdChange: '&',
sdChangeDelay: '#' //optional
},
link: function(scope, elm, attr) {
if (attr.type === 'radio' || attr.type === 'checkbox') {
return;
}
if (!scope.sdChangeDelay) {
scope.sdChangeDelay = 500; //defauld delay
}
var parameters = getParameters(attr.sdChange);
var delayTimer;
elm.bind('keydown keypress', function() {
if (delayTimer !== null) {
$timeout.cancel(delayTimer);
}
delayTimer = $timeout(function() {
var params = {};
if (parameters && parameters.length > 0) {
for (var n = 0; n < parameters.length; n++) {
params[parameters[n]] = scope.$parent.$eval(parameters[n]);
}
} else {
params = null;
}
if (params == null) {
scope.sdChange();
} else {
scope.sdChange(params)
}
delayTimer = null;
}, scope.sdChangeDelay);
scope.$on(
"$destroy",
function(event) {
$timeout.cancel(delayTimer);
console.log("Destroyed");
}
);
});
}
};
}
]);
function getParameters(functionStr) {
var paramStr = functionStr.slice(functionStr.indexOf('(') + 1, functionStr.indexOf(')'));
var params;
if (paramStr) {
params = paramStr.split(",");
}
var paramsT = [];
for (var n = 0; params && n < params.length; n++) {
paramsT.push(params[n].trim());
}
return paramsT;
}
plnkrs for both approaches are
http://plnkr.co/edit/r5t0KwMtNeOhgnaidKhS?p=preview
http://plnkr.co/edit/9PGbYGCDCtB52G8bJkjx?p=info
how about this plunkr?
using angular's built in ng-blur, update your "persisted value" on blur
<input type="text" ng-model="boxValue" ng-blur="doneEditing(boxValue)">
when saving, verify the value is different
$scope.doneEditing = function(v) {
if (v !== $scope.persistedValue) // only save when value is different
$scope.persistedValue=v;
}
There is no special option on ng-blur for pre-checking equality that I'm aware of. A simple if seems to do the trick
Newer versions of AngularJS (now in 1.3 beta) supports this natively. See my answer here
The solution that worked for me was as follows:
<input id="fieldId" type="text"
ng-model="form.fields.thisField"
ng-model-options="{ updateOn: 'blur' }"
ng-change="form.save()" />
I found this solution here
That page says it requires AngularJS version 1.3.0+ . I tested it with AngularJS version 1.5.11 and it works in that version for me.
In app.html
<input type="text" name="name" ng-model="$ctrl.user.name" ng-blur="$ctrl.saveChanges()" ng-change="$ctrl.onFieldChange()"/>
in app.ts
public onFieldChange() {
this.isValuesModified = true;
}
//save changes on blur event
public saveChanges() {
//check if value is modified
if(this.isValuesModified){
//Do your stuff here
}
}
This is how I resolved this.
<input ng-focus="oldValue=value" ng-model="value" ng-blur="oldValue!=value && youFunction()" />

Categories

Resources