How to unit test a key press event in angular directive - angularjs

I've created an edit directive to wrap an html input in a fancy frame.
Now I'm creating a unit test to check that once I type in the input the dirty state of the form controller is set.
This is what I got so far but it fails on the 2nd expect.
What's wrong here?
Thanks in advance!
describe('dirty', function () {
it('false', function () {
var scope = $rootScope.$new(),
form = $compile('<form name="form"><edit ng-model="model"></edit></form>')(scope),
input = form.find('input');
scope.$digest();
expect(scope.form.$dirty).toBeFalsy();
input.triggerHandler('keydown', { which: 65 });
expect(scope.form.$dirty).toBeTruthy();
});
});
Edit:
For all that matters it comes down to this plunker (no jQuery) ... or this one using jQuery
Any idea?

The angular unit tests in ngKeySpec.js were helpful:
it('should get called on a keydown', inject(function($rootScope, $compile) {
element = $compile('<input ng-keydown="touched = true">')($rootScope);
$rootScope.$digest();
expect($rootScope.touched).toBeFalsy();
browserTrigger(element, 'keydown');
expect($rootScope.touched).toEqual(true);
}));

Related

angularjs + jasmine : testing focus in a service

Learning jasmine for the first time and I am stuck on this error when trying to test the focus() functionality in an angular service.
Here is the service:
myApp.service('MyService', function($timeout, $window) {
var service = {
focusElem: focusElem
};
return service;
function focusElem(id) {
console.log('id of element is = ', id);
if (id) {
$timeout(function() {
var element = $window.document.getElementById(id);
console.log('element is = ', element);
if (element) {
element.focus();
}
});
}
};
});
Here is my spec file
describe('myApp', function() {
var element, dummyElement;
beforeEach(function() {
// Initialize myApp injector
module('myApp');
// Inject instance of service under test
inject(function($injector) {
MyServiceObj = $injector.get('MyService');
});
element = angular.element('<input id="firstName" name="firstName"/>');
dummyElement = document.createElement('input');
dummyElement.setAttribute('id', 'lastName');
});
it('should have focus if the focus Service is used on an element', function() {
console.info('------------------');
spyOn(element[0], 'focus');
spyOn(dummyElement, 'focus');
MyServiceObj.focusElem(dummyElement.getAttribute('id'));
expect(dummyElement.focus).toHaveBeenCalled();
});
});
My error:
myApp should have focus if the focus Service is used on an element
Expected spy focus to have been called.
Error: Expected spy focus to have been called.
If you are using ngMock many services are changed so they can be controlled in a synchronous manner within test code to give you more control over the flow.
One of the affected services is $timeout.
The function passed to $timeout inside your service will not execute in your test unless you tell it to.
To tell it to execute use $timeout.flush() like this:
spyOn(element[0], 'focus');
spyOn(dummyElement, 'focus');
MyServiceObj.focusElem(dummyElement.getAttribute('id'));
$timeout.flush();
expect(dummyElement.focus).toHaveBeenCalled();
Note that you need a reference to the $timeout service:
var element, dummyElement, $timeout;
beforeEach(function() {
module('myApp');
inject(function($injector, _$timeout_) {
MyServiceObj = $injector.get('MyService');
$timeout = _$timeout_;
});
The next problem is due to the following line in your service:
var element = $window.document.getElementById(id);
The elements you create in your test are never attached to the DOM, so the service will not find them.
The easiest solution is to just attach your elements to the DOM. In this case it's important that you remove them manually after the test, since Jasmine uses the same DOM for your entire test suite.
For example:
it('should have focus if the focus Service is used on an element', function() {
var body = angular.element(document.body);
body.append(element);
body.append(dummyElement);
spyOn(element[0], 'focus');
spyOn(dummyElement, 'focus');
MyServiceObj.focusElem(dummyElement.getAttribute('id'));
$timeout.flush();
expect(dummyElement.focus).toHaveBeenCalled();
element.remove();
dummyElement.remove();
});
Demo: http://plnkr.co/edit/F8xqfYYQGa15rwuPPbN2?p=preview
Now, attaching and removing elements to the DOM during unit tests are not always a good thing to do and can get messy.
There are other ways to handle it, for example by spying on getElementById and controlling the return value or by mocking an entire document. I won't go into that here however as I'm sure there are examples of it around here already.

Angular Testing DOM after update by Factory

Ok, I'm trying to test the outcome of a function that updates the DOM>
I have a directive that loads a template via url.
Then a controller calls a factory method to update the html table with data.
I have the tests showing that I can get the data that is all good.
but how can I test that the updates to the table have taken place?
I am using NodeJS with Karma and Jasmine.
I have followed tutorials on how to load in templates, and I have that working, I can load and access the templates in my test fine.
but when I run the method to update the table, the tests fail.
I'll give an scaled down example of what I'm trying to do. Note, this is just demo code, Not a working app.
Template.
<table><tr><td class="cell1"></td></tr></table>
Directive.
dataTable.directive('dataTable', function () {
return {
restrict: 'E',
templateUrl: 'path/to/template/dataTable.html'
};
});
Controller
dataTable.controller('dataTableController', ['$scope', 'dataTableFactory',
function ($scope, dataTableFactory){
$scope.updateTable = function(){
dataTableFactory.loadData();
// code to load data from dataTableFactory here! //
dataTableFactory.updateTable();
}
}])
Factory
dataTable.factory('dataTableFactory',['$document',function($document){
var _tableData;
return(
"tableData": _tableData,
loadData: function(){
// code to get data and populate _tableData.
}
updateTable: function(){
$document.find('.cell1').append(this.tableData.data);
}
)
}])
Unit Test
describe('dataTable Tests', function () {
var scope, element, $compile, mDataTableFactory, controller, tableData, doc, factory;
beforeEach(module('dataTable'));
beforeEach(module('app.templates')); // setup via ng-html2js
beforeEach(inject(function (_$rootScope_, _$compile_,_$controller_,_dataTableFactory_) {
scope = _$rootScope_.$new();
doc = _$compile_('<flood-guidance></flood-guidance>')(scope);
factory = _dataTableFactory_;
controller = _$controller_('dataTableController', {
$scope: scope,
$element: doc,
dataTableFactory: factory
});
scope.$digest();
}));
it("Template should contain the cell cell1", function(){
expect(doc.find('.cell1').contents().length).toBe(0);
expect(doc.find('.cell1').html()).toBeDefined();
});
// Passes fine, so I know the template loads ok.
it('Should show data in cell1',function(){
factory.tableData = {data: 'someData'};
scope.updateTable();
expect(doc.find('.cell1').contents().length).toBe(1);
expect(doc.find('.cell1').html()).toBe('SomeData');
});
});
});
Test Ouptut
Expected 0 to be 1. Expected '' to be 'someData'.
If I put the updateTable code in to the controller and call the update function there, the test passes, but I'd like to have this in a factory, how can I make this test pass (the app runs and works as expected, I just can't get a working test).
I understand this kind of testing is more focused on the UI and not exactly 'Unit Testing' but is it possible to do this?
So essentially updateTable cannot find the changes performed by factory.tableData. I guess the problem may be due to the way how your factory exposes the _tableData property.
Could you try to modify your factory like this:
dataTable.factory('dataTableFactory',['$document',function($document){
var _tableData;
return(
getTableData: function() { return _tableData; },
setTableData: function(newVal) { _tableData = newVal; },
loadData: function(){
// code to get data and populate _tableData.
}
updateTable: function(){
$document.find('.cell1').append(this.tableData.data);
}
)
}])
and then of course use the setter/getter accordingly. See if it works this way.
OK so I'm still not sure if I fully get your intention but here is a fiddle with my refactored example.
http://jsfiddle.net/ene4jebb/1/
First of all the factory shouldn't touch the DOM, that's the directives responsibility. Thus my rework passes the cellvalue (new scope property) to the directive, which renders it. Now when you call setTableData (which will change _tableData.data) and since in test environment call the $digest loop yourself, the directive will automatically redraw the new stuff.
Controller is kept thin as possible thus only providing a scope property to the factory.
As said not sure if you were after this, but hope it helps. If there are any questions just ask.

Trigger click event on an AngularJS directive in Mocha test suite

I have a regular angular app with a directive. This directive contains an element with a ng-click="clickFunction()" call. All works well when I click that element. I now need to write a test for this click, making sure that this function was actually run when the element was clicked - this is what I'm having trouble with.
Here's a jsfiddle to illustrate my issue: http://jsfiddle.net/miphe/v0ged3vb/
The controller contains a function clickFunction() which should be called on click. The unit test should imitate a click on the directive's element and thus trigger the call to that function.
The clickFunction is mocked with sinonjs so that I can check whether it was called or not. That test fails, meaning there was no click.
What am I doing wrong here?
I've seen the answer to similar questions like Testing JavaScript Click Event with Sinon but I do not want to use full jQuery, and I believe I'm mocking (spying on) the correct function.
Here's the js from the fiddle above (for those who prefer to see it here):
angular.js, angular-mocks.js is loaded as well.
// App
var myApp = angular.module('myApp',[]);
myApp.controller('MyCtrl', function($scope) {
$scope.person = 'Mr';
$scope.clickFunction = function() {
// Some important functionality
};
});
myApp.directive('pers', function() {
return {
restrict: 'E',
template: '<h2 ng-click="clickFunction()" ng-model="person">Person</h2>',
};
});
// Test suite
describe('Pers directive', function() {
var $scope, $controller, template = '<pers></pers>', compiled;
beforeEach(module('myApp'));
beforeEach(inject(function($rootScope, $controller, $compile) {
$scope = $rootScope.$new();
ctrl = $controller('MyCtrl', {$scope: $scope});
compiled = $compile(template)($scope);
// Do I need to run a $scope.$apply() here?
console.log($scope.$apply); // This is a function, apparently.
//$scope.$apply(); // But running it breaks this function.
}));
it('should render directive', function() {
el = compiled.find('h2');
expect(el.length).to.equal(1);
});
it('should run clickFunction() when clicked', function() {
el = compiled.find('h2');
sinon.spy($scope, 'clickFunction');
// Here's the problem! How can I trigger a click?
//el.trigger('click');
//el.triggerHandler('click');
expect($scope.clickFunction.calledOnce).to.be.true
});
});
// Run tests
mocha.run();
Turns out the problem was quite hidden.
Firstly the $scope.$digest and $scope.$apply functions broke the beforeEach function which ultimately led to the whole solution.
Solution
Do not mix angular versions.
In the first fiddle
angular.js version 1.3.0
angular-mocks.js version 1.1.5
In the solved fiddle
angular.js version 1.3.0
angular-mocks.js version 1.3.0
That was the whole problem, and gave me quite obscure errors.
Thanks to Foxandxss from the #AngularJS IRC channel on freenode.
The way to trigger events on the directive with jQlite was simply:
someElement.triggerHandler('click');

Angular tests: How to expect element events to be triggered?

I'm decorating forms like this:
angular.module('Validation').directive('form', function() {
return {
restrict: 'E',
link: function(scope, element) {
var inputs = element[0].querySelectorAll('[name]');
element.on('submit', function() {
for (var i = 0; i < inputs.length; i++) {
angular.element(inputs[i]).triggerHandler('blur');
}
});
}
};
});
Now, I'm trying to test this directive:
describe('Directive: form', function() {
beforeEach(module('Validation'));
var $rootScope, $compile, scope, form, input, textarea;
function compileElement(elementHtml) {
scope = $rootScope.$new();
form = angular.element(elementHtml);
input = form.find('input');
textarea = form.find('textarea');
$compile(form)(scope);
scope.$digest();
}
beforeEach(inject(function(_$rootScope_, _$compile_) {
$rootScope = _$rootScope_;
$compile = _$compile_;
compileElement('<form><input type="text" name="email"><textarea name="message"></textarea></form>');
}));
it('should trigger "blur" on all inputs when submitted', function() {
spyOn(input, 'trigger');
form.triggerHandler('submit');
expect(input.trigger).toHaveBeenCalled(); // Expected spy trigger to have been called.
});
});
But, the test fails.
What's the right Angular way to test this directive?
You have some problems:
1) input = form.find('input'); and angular.element(inputs[i]); are 2 different wrapper objects wrapping the same underlying DOM object.
2) You should create a spy on triggerHandler instead.
3) You're working directly with DOM which is difficult to unit-test.
An example of this is: angular.element(inputs[i]) is not injected so that we have difficulty faking it in our unit tests.
To ensure that point 1) returns the same object. We can fake the angular.element to return a pre-trained value which is the input = form.find('input');
//Jasmine 1.3: andCallFake
//Jasmine 2.0: and.callFake
angular.element = jasmine.createSpy("angular.element").and.callFake(function(){
return input; //return the same object created by form.find('input');
});
Side note: As form is already an angularJs directive, to avoid conflicting with an already defined directive, you should create another directive and apply it to the form. Something like this:
<form mycustomdirective></form>
I'm not sure if this is necessary. Because we're spying on a global function (angular.element) which may be used in many places, we may need to save the previous function and restore it at the end of the test. Your complete source code looks like this:
it('should trigger "blur" on all inputs when submitted', function() {
var angularElement = angular.element; //save previous function
angular.element = jasmine.createSpy("angular.element").and.callFake(function(){
return input;
});
spyOn(input, 'triggerHandler');
form.triggerHandler('submit');
angular.element = angularElement; //restore
expect(input.triggerHandler).toHaveBeenCalled(); // Expected spy trigger to have been called.
});
Running DEMO
This is probably something to do with raising the 'submit' event during the test.
The angular team have created a pretty funky class to help them do this it seems to cover a lot of edge cases - see https://github.com/angular/angular.js/blob/master/src/ngScenario/browserTrigger.js
While this helper is from ngScenario I use it in my unit tests to overcome problems raising some events in headless browsers such as PhantomJS.
I had to use this to test a very similar directive that performs an action when a form is submitted see the test here https://github.com/jonsamwell/angular-auto-validate/blob/master/tests/config/ngSubmitDecorator.spec.js (see line 38).
I had to use this as I am using a headless browser for development testing purposes. It seems that to trigger an event in this type of browser the element that is triggering the event has to be attached to the dom as well.
Also as the form directive is one that angular already has you should either decorate this directive or give this directive a new name. I would actually suggest you decorate the ngSubmit directive instead of the form directive as this is more gear towards submitting a form. I actually have a very good example of this as I did this in the validation module I have open sourced. This should give you a very good start.
The directive source is here
The directive tests are here
Try hooking into the blur event:
it('should trigger "blur" on all inputs when submitted', function() {
var blurCalled = false;
input.on('blur', function() { blurCalled = true; });
form.triggerHandler('submit');
expect(blurCalled).toBe(true);
});

How do I check if my element has been focussed in a unit test

I have the following directive to autofocus a field:
.directive('ngAutofocus', function ($timeout) {
return {
restrict: 'A',
link: function (scope, elm) {
$timeout(function () {
elm[0].focus();
});
}
};
}
How would I unit test this? I tried several things like the following selector but they all return errors or false:
console.log($(elm[0]).is(':focus'));
My unit test is set up like this:
elm = angular.element('<input type="text" name="textfield1" ng-autofocus>');
$scope.$digest();
$compile(elm)($scope);
I figured it out, and it was pretty obvious actually;
it('should set the focus on timeout', function () {
spyOn(elm[0],'focus');
$timeout.flush();
expect(elm[0].focus).toHaveBeenCalled();
})
My problem was two-fold:
I wasn't calling the timeout flush function so timeout wasn't occuring, and
I was trying to look at the focus attribute of the element whereas just looking at the call of the focus() function is much more like unit-testing. The focus attribute is something that really belongs to the e2e testing territory.
You can use document.activeElement to check focus. The only downside being that the HTML needs to be added to the document body for this to work.
https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement
A more verbose solution below, which allows testing (spying on) the focus that runs immediately (i.e. no $timeout or other event). The key is to first render a DOM element before $compile runs:
'use strict';
describe('Testing the focus call from the link function', function () {
var $compile;
var $rootScope;
beforeEach(angular.mock.module('auto-focus-module'));
beforeEach(inject(function (_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
it('should automatically focus when calling the link function', function () {
var $scope = $rootScope.$new();
// create an uncompiled DOM element so we can bind the focus spy
var rawEl = angular.element('<input auto-focus-directive>');
// set the spy
spyOn(rawEl[0], 'focus');
// compile the rawEl so that compile and link functions run
$compile(rawEl)($scope);
expect(rawEl[0].focus).toHaveBeenCalled();
});
});
With a directive and link function that could look like:
(function () {
'use strict';
angular.module('auto-focus-module')
.directive('autoFocusDirective', autoFocusDirective);
function autoFocusDirective () {
return {
link: link
};
function link (scope, elem) {
elem[0].focus();
}
}
})();
You should use the angular.element api - jQuery lite - and use the method triggerHandler().
it('should have focus', function() {
elm.triggerHandler('focus');
expect(elm).toBeInFocus() //PSEUDO CODE - you will need to see how this can be tested
}
http://docs.angularjs.org/api/ng/function/angular.element
http://api.jquery.com/triggerhandler/
Potential Area for some testing focus knowledge:
https://shanetomlinson.com/2014/test-element-focus-javascript
Also you concerning your unit test - you don't need to append the element to the body, it's possible to test without that.

Resources