How to test a directive containing a text field in Angularjs - angularjs

I have a directive containing a text field, and I want to test to make sure that the text entered into the field makes it to the model.
The directive:
define(function(require) {
'use strict';
var module = require('reporting/js/directives/app.directives');
var template = require('text!reporting/templates/text.box.tpl');
module.directive('textField', function () {
return {
restrict: 'A',
replace: true,
template:template,
scope: {
textField : "=",
textBoxResponses : "="
},
link: function(scope) {
scope.debug = function () {
scope;
// debugger;
};
}
};
});
return module;
});
The markup:
<div ng-form name="textBox">
<!-- <button ng-click="debug()">debug the text box button</button> -->
<h1>Text Box!</h1>
{{textField.label}} <input type="text" name="textBox" ng-model="textBoxResponses[textField.fieldName]">{{name}}
</div>
The test code:
/* global inject, expect, angular */
define(function(require){
'use strict';
require('angular');
require('angularMock');
require('reporting/js/directives/app.directives');
require('reporting/js/directives/text.box.directive');
describe("builder experimenter", function() {
var directive, scope;
beforeEach(module('app.directives'));
beforeEach(inject(function($compile, $rootScope) {
scope = $rootScope;
scope.textBoxResponses = {};
scope.textBoxField = {
fieldName : "textBox1"
};
directive = angular.element('<div text-field="textBoxField" text-box-responses="textBoxResponses"></div>');
$compile(directive)(scope);
scope.$digest();
}));
it('should put the text box value on the model', inject(function() {
directive.find(":text").val("something");
expect(scope.textBoxResponses.textBox1).toBe("something");
}));
});
});
So, what I'm trying to do in the last it block is to simulate typing in the text field, and then check to make sure that the new value of the text field makes it to the model. The issue is that the model is never updated with the new value.

The issue is ng-model is never informed that anything is in the textfield. ng-model is listening for the input event. All you have to do to fix your code is:
var text = directive.find(":text");
text.val("something");
text.trigger('input');
expect(scope.textBoxResponses.textBox1).toBe("something");
When the ng-model gets the event input, then check your scope and everything will be what you expect.

I got this done by using the sniffer service.
Your test will look like this:
var sniffer;
beforeEach(inject(function($compile, $rootScope, $sniffer) {
scope = $rootScope;
sniffer = $sniffer;
scope.textBoxResponses = {};
scope.textBoxField = {
fieldName : "textBox1"
};
directive = angular.element('<div text-field="textBoxField" text-box-responses="textBoxResponses"></div>');
$compile(directive)(scope);
scope.$digest();
}));
it('should put the text box value on the model', inject(function() {
directive.find(":text").val("something");
directive.find(":text").trigger(sniffer.hasEvent('input') ? 'input' : 'change');
expect(directive.isolateScope().textBoxResponses.textBox1).toBe("something");
}));
I found this pattern here: angular-ui-bootstrap typeahead test
The trigger basically makes the view value go into the model.
Hope this helps

Related

Test case for focus-next custom directive in Angular JS

I created a custom directive to resolve some focus related issues in my application.
Directive Code:
(function() {
angular.module("FocusNextModule", []).directive("focusNext", function() {
return {
restrict: "A",
link: function($scope, elem, attrs) {
elem.bind("focus", function(e) {
var code = e.which || e.keyCode;
var nextElem = document.getElementById(attrs.focusNext);
if (nextElem === null || nextElem === undefined) {
var altElem = document.getElementById(attrs.focusNextAlt);
if (angular.element(altElem).hasClass('ng-hide') === false) {
altElem.focus();
} else {
var selfElem = document.getElementById(attrs.focusSelf);
selfElem.focus();
}
e.preventDefault();
} else {
nextElem.focus();
e.preventDefault();
}
});
}
};
});
})();
How to use in template Use InT emplate
<md-button id="idOfElementC">MyButton</md-button>
<div tabindex="0" focus-next="idOfElementA" focus-next-alt="idOfElementB" focus-self="idOfElementC"></div>
Note:
Element with "idOfElementC" id will be just above the div using focus-next directive.
How does the directive work?
When we press tab on element with "idOfElementC" id (here button), focus will go to div using focus-next directive. The div will redirect the focus to other elements using following cases:
a) First it will check if there is any element with id "idOfElementA". If element exists, then that element will receive focus.
b) If element with id "idOfElementA" do not exist, then "idOfElementB" will receive focus.
c) If element with id "idOfElementB" do not exist as well, then finally "idOfElementA" (on which tab was pressed) will receive focus.
The directive is working fine and fixing all my issues. But, I need to write jasmine test cases for this directive.
Can anyone guide me how to write Jasmine test cases for focus?
UPDATE:
As per comment of #PetrAveryanov the directive was looking horrible and I completely agree.
Updated Directive:
(function() {
angular.module("FocusNextModule", []).directive("focusNext", function() {
return {
restrict: "A",
link: function($scope, elem, attrs) {
elem.bind("focus", function(e) {
var elemToFocus = document.getElementById(attrs.focusNext) || document.getElementById(attrs.focusNextAlt);
/*jshint -W030 */
angular.element(elemToFocus).hasClass('ng-hide') === false ? elemToFocus.focus() : document.getElementById(attrs.focusSelf).focus();
e.preventDefault();
});
}
};
});
})();
Finally, got it how to write test cases for the directive.
describe('focus-next-directive test', function() {
var compile, scope;
beforeEach(module(FocusNextModule));
beforeEach(inject(function($compile, $rootScope) {
compile = $compile;
scope = $rootScope.$new();
}));
it('should focus the next element', function() {
var div = compile('<div tabindex="0" focus-next="idTestNext"/>')(scope);
var nextElem = compile('<input id="idTestNext" type="text" />')(scope);
angular.element(document.body).append(div);
angular.element(document.body).append(nextElem);
div.focus();
expect(nextElem).toEqual(angular.element(document.activeElement));
div.remove();
nextElem.remove();
});
it('should focus the next alternative element', function() {
var div = compile('<div tabindex="0" focus-next="idTestNext" focus-next-alt="idTestNextAlt"/>')(scope);
var nextAltElem = compile('<input id="idTestNextAlt" type="text" />')(scope);
angular.element(document.body).append(div);
angular.element(document.body).append(nextAltElem);
div.focus();
expect(nextAltElem).toEqual(angular.element(document.activeElement));
div.remove();
nextAltElem.remove();
});
it('should focus the Self element', function() {
var selfElem = compile('<input id="idTestSelf" type="text" ng-class="ng-hide"/>')(scope);
var div = compile('<div tabindex="0" focus-next="idTestNext" focus-next-alt="idTestNextAlt" focus-self="idTestSelf"/>')(scope);
var nextAltElem = compile('<input id="idTestNextAlt" type="text" class="ng-hide"/>')(scope);
angular.element(document.body).append(selfElem);
angular.element(document.body).append(div);
angular.element(document.body).append(nextAltElem);
div.focus();
expect(selfElem).toEqual(angular.element(document.activeElement));
div.remove();
selfElem.remove();
nextAltElem.remove();
});
});

Custom directive not picking up model initialization

I have written a custom AngularJS directive that implements a toggle checkbox based on Bootstrap Toggle. I added support for angular-translate, but that is beyond my actual proplem. Furthermore, I wanted to use angular-cookies to save and restore the current state of a particular checkbox.
However, my directive does not pick-up the initial value of the data model properly.
This is my directive:
app.directive('toggleCheckbox', ['$rootScope', '$translate', '$timeout', function($rootScope, $translate, $timeout) {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attributes, ngModelController) {
// Change model, when checkbox is toggled
element.on('change.toggle', function(event) {
var checked = element.prop('checked');
console.log('change.toggle was called: ' + checked + ' vs. ' + ngModelController.$viewValue);
if (checked != ngModelController.$viewValue) {
scope.$apply(function changeViewModel() {
ngModelController.$setViewValue(checked);
console.log('change.toggle:', checked);
});
}
});
// Render element
ngModelController.$render = function() {
element.bootstrapToggle(ngModelController.$viewValue ? 'on' : 'off')
};
// Translate checkbox labels
var updateLabels = function() {
var offLabel = (attributes['off'] ? $translate.instant(attributes['off']) : 'Off');
var onLabel = (attributes['on'] ? $translate.instant(attributes['on']) : 'On');
angular.element(document).find('label.btn.toggle-off').html(offLabel);
angular.element(document).find('label.btn.toggle-on').html(onLabel);
};
// Update labels, when language is changed at runtime
$rootScope.$on('$translateChangeSuccess', function() {
updateLabels();
});
// Initialize labels for the first time
$timeout(function() {
updateLabels();
});
// Clean up properly
scope.$on('$destroy', function() {
element.off('change.toggle');
element.bootstrapToggle('destroy');
});
// Initialize element based on model
var initialValue = scope.$eval(attributes.ngModel);
console.log('initialValue:', initialValue);
element.prop('checked', initialValue);
}
};
}]);
This is how I initialize the data model from cookie:
mainController.controller('MainCtrl', ['$scope', '$cookies', 'Main', function($scope, $cookies, Main) {
this.$onInit = function() {
$scope.settings.foobar = $cookies.get('foobar');
console.log('$onInit(): ', $scope.settings.foobar);
};
// ...
}]);
And this is how I eventually use my directive:
<div id="foobar-switcher" ng-if="isAdmin()">
<label for="foobar_toggle"><span translate="foobar"></span>:</label>
<input id="foobar_toggle" type="checkbox"
ng-model="settings.foobar" ng-change="setFoobarCookie(settings.foobar)" toggle-checkbox
data-off="foo_label" data-offstyle="success"
data-on="bar_label" data-onstyle="danger" />
</div>
Ultimately, I get this debug output:
controllers.js:33 $onInit(): true
directives.js:76 initialValue: true
directives.js:37 change.toggle was called: false vs. false
So in case the value true is stored in the cookie, the model gets initialized properly in the global scope and even the directive uses the correct value to initialize element. The change.toggle event handler is triggered as well, but there the element's value is now false.
Why is that and how can I fix this problem?
It turned out that $cookies.get('foobar') returns a String and the checkbox cannot handle a ng-model of that type. Instead, the cookie value must be evaluated such, that a boolean value can be set in the data model:
var cookie = $cookies.get('foobar');
$scope.settings.foobar = (cookie === 'true');
The checkbox is then correctly initialized on page load.

Angular directive to use ng-blur and ng-focus

I'm having trouble understanding Angular directives. I'd like to use a simple attribute to expand into more complicated html, but with some parts of this template being replaceable via parameters.
Given this code:
<form role="form" name="databaseForm">
<input type="text" name="connectionName"
ng-focus="databaseForm.connectionName.$focused = true;databaseForm.connectionName.$blurred = false;"
ng-blur="databaseForm.connectionName.$blurred = true;databaseForm.connectionName.$focused = false;"
>
</form>
I'd like to write it using a more terse directive (such as "blurred-focused"):
<form role="form" name="databaseForm">
<input type="text" name="connectionName"
blurred-focused="'databaseForm.connectionName'"
>
</form>
So that means I can very easily re-use it for other inputs:
<form role="form" name="databaseForm">
<input type="text" name="username"
blurred-focused="'databaseForm.username'"
>
</form>
The expected result from this is that the inputs with this directive will have the $blurred and $focused properties automatically applied to it, based on the user interaction with the input.
Thank you.
Update:
I ended up using MMHunter's version where the scope is non-isolated. I added some logic to automatically find the form and field object, so that I didn't need to specify it all.
My directive:
(function () {
"use strict";
angular
.module("app.shared.widgets")
.directive("blurredFocused", blurredFocused);
blurredFocused.$inject = ["_"];
/* #ngInject */
function blurredFocused(_) {
return {
restrict: "A",
priority: -1,
link: function(scope, element, attributes) {
element.on("blur", function () {
var formFieldName = element[0].form.name + "." + element[0].name;
var target = _.get(scope, formFieldName);
scope.$apply(function() {
target.$blurred = true;
target.$focused = false;
});
});
element.on("focus", function () {
var formFieldName = element[0].form.name + "." + element[0].name;
var target = _.get(scope, formFieldName);
scope.$apply(function() {
target.$blurred = false;
target.$focused = true;
});
});
}
};
}
})();
And an example of its usage:
<form role="form" name="databaseForm">
<input type="text" name="connectionName" blurred-focused>
</form>
You need is not difficult to achieve with angular directive. But things can be different based on whether isolated scope is used.
With isolated scope, the following code is straightForward. Binding the value to an isolated scope in the 'blurred-focused' directive and listen to the events.
//with isolated scope
app.directive("blurredFocused", [function () {
return {
restrict:"A",
priority:-1,
scope:{
blurredFocused:"="
},
link:function(scope,ele,attrs){
ele.on("blur",function(){
scope.$apply(function(){
scope.blurredFocused.$blurred = true;
scope.blurredFocused.$focused = false;
})
})
ele.on("focus",function(){
scope.$apply(function(){
scope.blurredFocused.$blurred = false;
scope.blurredFocused.$focused = true;
})
})
}
}
}]);
But without isolated scope, things could be a little bit tricky. we need to find the scope value manually by the attributes value.
//without isolated scope
app.directive("blurredFocused", [function () {
return {
restrict:"A",
priority:-1,
link:function(scope,ele,attrs){
ele.on("blur",function(){
var targetField = scope[attrs.blurredFocused];
scope.$apply(function(){
targetField.$blurred = true;
targetField.$focused = false;
})
})
ele.on("focus",function(){
var targetField = scope[attrs.blurredFocused];
scope.$apply(function(){
targetField.$blurred = false;
targetField.$focused = true;
})
})
}
}
}]);
Here is the plunker
I would recommend you use the one without isolated scope. Attribute directives are always used together so it may not be a good idea to have isolated scopes.

AngularJS: How to set a controller property from a directive that uses a child scope?

LIVE DEMO
Consider the following spinner-click directive:
Directive Use:
<button class="btn btn-mini"
ng-class="{'btn-warning': person.active, disabled: !person.active}"
spinner-click="deleteItem($index)"
spinner-text="Please wait..."
spinner-errors="alerts">
Delete
</button>
Directive:
app.directive('spinnerClick', function() {
return {
restrict: 'A',
scope: true,
link: function(scope, element, attrs) {
var originalHTML = element.html();
var spinnerHTML = "<i class='icon-refresh icon-spin'></i> " + attrs.spinnerText;
element.click(function() {
if (element.is('.disabled')) {
return;
}
element.html(spinnerHTML).addClass('disabled');
scope.$apply(attrs.spinnerClick).then(function() {
element.html(originalHTML).removeClass('disabled');
}, function(errors) {
element.html(originalHTML).removeClass('disabled');
// This is ugly! Is there a better way?
var e = scope[attrs.spinnerErrors];
e.length = 0;
e.push.apply(e, errors);
});
});
}
};
});
Controller:
app.controller('MainCtrl', function($scope, $q, $timeout) {
$scope.alerts = ['First alert'];
$scope.people = [
{ name: 'David', active: true },
{ name: 'Layla', active: false }
];
$scope.deleteItem = function(index) {
var defer = $q.defer();
$timeout(function() {
defer.reject(["Something 'bad' happened.", "Check your logs."]);
}, 2000);
return defer.promise;
};
});
Note: spinner-click can be used with other directives (e.g. ng-class in this example).
As you can see, I set $scope.alerts in the directive using a very nasty way. Can you find a better way to do this?
UPDATE: (DEMO)
I tried to use $parse like this:
var errorsModel = $parse(attrs.spinnerErrors);
errorsModel.assign(scope, errors);
and this doesn't work.
BUT, if I have spinner-errors="wrapper.alerts" rather than spinner-errors="alerts", it does work!
Is there a way to avoid using the wrapper?
I think you can do it more simply using an isolate scope.
Instead of scope: true,, you should put:
scope:{
spinnerClick:"&",
spinnerText : "#",
spinnerErrors: "="
}
And then, in your directive use scope.spinnerClick, scope.spinnerText , scope.spinnerErrors directly.
The & is used to bind function expression defined in your attribute and pass it to your directive's scope, the # will bind the text value of the attribute and the = will set a double binding with the expression passed in the attribute.
You can fine a more precise explanation here http://docs.angularjs.org/guide/directive (look at the long version), and a much clearer explanation here http://www.egghead.io/ (look at the isolate scope videos, it only takes a few minutes and makes it look so simple).
To answer your original question regarding the ugliness of
// This is ugly! Is there a better way?
var e = scope[attrs.spinnerErrors];
e.length = 0;
e.push.apply(e, errors);
You can use angular.copy to achieve the same results
angular.copy(errors, scope[attrs.spinnerErrors]);
The reason this is so ugly in your directive is due to your use of a child scope. If you did not create a child scope, or were willing to create an isolation scope this would not be a problem. You can't use $scope.alerts because
The child scope gets its own property that hides/shadows the parent
property of the same name. Your workarounds are
define objects in the parent for your model, then reference a property of that object in the child: parentObj.someProp
use $parent.parentScopeProperty (not always possible, but easier than 1. where possible)
define a function on the parent scope, and call it from the child (not always possible)
There is a detailed explanation that can be found here.
One option is to create a setter function in the controller that you can call in the directive. The reference to the function in the child scope could then be used set the value in the parent scope. Another option is to create an isolation scope and then pass in a setter function using & binding.
You had the right idea with $parse. The problem is that you're assigning the new array of errors to the child scope, which hides (but doesn't replace) the array on the parent/controller scope.
What you have to do is get a reference to the parent array and then replace the contents (like you were doing in the original version). See here.
I question the need to put the error logic within the directive. You can simply handle the error as part of the controller. Unless you absolutely need to have the html replaced and class removed before manipulating the alerts array, your code could be rewritten as:
app.directive('spinnerClick', function() {
return {
restrict: 'A',
scope: true,
link: function(scope, element, attrs) {
var originalHTML = element.html();
var spinnerHTML = "<i class='icon-refresh icon-spin'></i> " + attrs.spinnerText;
function onClickDone(){
element.html(originalHTML).removeClass('disabled');
}
element.click(function() {
if (element.is('.disabled')) {
return;
}
element.html(spinnerHTML).addClass('disabled');
scope.$apply(attrs.spinnerClick).then(onClickDone, onClickDone);
});
}
};
});
app.controller('MainCtrl', function($scope, $q, $timeout) {
$scope.alerts = ['First alert'];
$scope.people = [
{ name: 'David', active: true },
{ name: 'Layla', active: false }
];
$scope.deleteItem = function(index) {
var defer = $q.defer();
$timeout(function() {
defer.reject(["Something 'bad' happened.", "Check your logs."]);
}, 2000);
return defer.promise.then(function(){
//Success handler
}, function(error){
$scope.alerts.length = 0;
$scope.alerts.push(error);
});
};
});

How to make $watch function executed in Test?

I am implementing a simple directive that represents a form field with all its extras like label, error field, regex all in a single line.
The directive is as follow:
<div ng-controller="parentController">
{{username}}
<!-- the directive -- >
<form-field label="Username:" regex="someRegex" constrainsViolationMessage="someValidationMessage" model="username" place-holder="some input value">
</form-field>
</div>
Now, I want to test the data binding between the directive scope and the parent scope.
The test is:
it("should bind input field to the scope variable provided by parent scope ! ", function () {
var formInput = ele.find('.form-input');
formInput.val("some input");
expect(ele.find('p').text()).toEqual('some input');
});
This problem is that I don't know why test don't pass, even the directive works correctly. Here is a fiddle of the directive.
And here is the whole test and test set up.
var formsModule = angular.module('forms', []);
formsModule.controller('parentController', function ($scope) {
});
formsModule.directive('formField', function () {
var label;
var constrainsViolationMessage;
var placeHolder;
var model;
return {
restrict:'E',
transclude:true,
replace:false,
scope:{
model:'='
},
link:function (scope, element, attr) {
console.log("link function is executed .... ");
scope.$watch('formInput', function (newValue, oldValue) {
console.log("watch function is executed .... !")
scope.model = newValue;
});
scope.label = attr.label;
},
template:'<div class="control-group ">' +
'<div class="form-label control-label">{{label}}</div> ' +
'<div class="controls controls-row"> ' +
'<input type="text" size="15" class="form-input input-medium" ng-model="formInput" placeholder="{{placeHolder}}">' +
'<label class="error" ng-show={{hasViolationConstrain}}>{{constrainsViolationMessage}}</label>' +
'</div>'
}
});
beforeEach(module('forms'));
var ele;
var linkingFunction;
var elementBody;
var scope;
var text = "";
var placeHolder = "filed place holder";
var label = "someLabel";
var regex = "^[a-z]{5}$";
beforeEach(inject(function ($compile, $rootScope) {
scope = $rootScope;
elementBody = angular.element('<div ng-controller="parentController">' +
'<p>{{username}}</p>' +
'<form-field label="Username:" regex="someRegex" constrainsViolationMessage="someValidationMessage" model="username" place-holder="some input value"> </form-field>');
ele = $compile(elementBody)(scope);
scope.$digest();
}
));
afterEach(function () {
scope.$destroy();
});
iit("should bind input field to the scope variable provided by parent scope ! ", function () {
var formInput = ele.find('.form-input');
formInput.val("some input");
expect(ele.find('p').text()).toEqual('some input');
});
As you can see, I want to assert that form input is reflected in the scope variable set in the 'model' attribute provided by the parent scope.
Am I missing something here ?
Thanks for helping me ... !
You're missing the scope.$apply() call after you set the input value, so the change is never getting digested. In the regular application lifecycle this would happen automatically, but you have to manually trigger this in your tests
Take a look at https://github.com/angular/angular.js/blob/master/test/ng/directive/formSpec.js for a ton of examples.
Use $scope.$digest() after adding condition to execute watch. It will fire watch.

Resources