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

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);
});

Related

Jamsine throws error despite same code working in browser

I added this code to my component controller to focus an input and it worked great in the browser but it broke all of my template tests. I thought I could just flush the $timeout and all would be well but it's not.
vm.$onInit = init;
function init(){
focusInput();
}
function focusInput(){
$timeout(function(){
$document[0]
.querySelector('md-autocomplete-wrap')
.querySelector('input')
.focus();
}, 0);
}
However, in my unit test Jasmine is reporting that .querySelector is not available because the result of the first querySelector is null in the test environment.
it('should render', function(){
var wrap, searchBarDirective, $scope;
$scope = $rootScope.$new();
searchBarDirective = $compile(angular.element(template))($scope);
$scope.$digest();
$timeout.flush();
wrap = searchBarDirective.find('md-autocomplete-wrap')[0];
expect(wrap).toBeDefined();
});
It's obvious to me that $document doesn't contain the rendered directive and thus the second querySelector fails. But why doesn't $document contain the directive?
I tried mocking querySelector with spyOn($document[0], "querySelector").and.returnValues($document[0],$document[0]) but that doesn't get me past the focus. Thinking I have lot my way here.
* Revised *
I think that it is important to continue to use $document but I decided to drop the querySelector for the jqLite find method.
function focusInput(){
$timeout(function(){
var input;
try {
// can throw an error if the first find fails
input = $document.find('md-autocomplete').find('input');
}
catch (e) {
angular.noop(e);
}
if(input && angular.isFunction(input.focus)) {
input.focus();
}
}, 0);
}
The test I changed per comments to below. I do have Karma load jquery to make testing easier which allows me to search for :focus
beforeEach(function(){
element = angular.element(template);
$document[0].body.appendChild(element[0]);
$scope = $rootScope.$new();
});
afterEach(function(){
element[0].remove();
});
it('should be focused', function(){
var input, searchBarDirective;
searchBarDirective = $compile(element)($scope);
$scope.$digest();
$timeout.flush();
input = searchBarDirective.find(':focus')[0];
expect(input).toBeDefined();
});
The reason why your querySelector call works in the browser, but not in tests is that you are creating a DOM element with angular.element, but you are never attaching it to the document. There are two ways to address this:
First, you could simply do this. Instead of:
searchBarDirective = $compile(angular.element(template))($scope);
Do:
let element; // declare this in the describe block so it is available later
element = angular.element(template);
document.body.appendChild(element[0]);
searchBarDirective = $compile(element)($scope);
And then do this:
afterEach(() => element[0].remove());
But, that's a bit messy. You should not be manipulating global scope (ie- the document) in your unit tests unless you have to. It would be better in your non-test code to avoid accessing the document and instead access a scope element, or some other DOM element that you can also mock in your tests. This will be a bit harder to do since it may require re-architecting your code a bit. In general though, in order to make modular and testable code, you want to avoid accessing the document object as much as possible.

AngularJS Unit Test element.bind

Inside a directive of mine called by data-type-ahead I have the following, in a series of events:
$scope.test = 5;
// Bind blur event and clear the value of
element.bind('blur', function(){
$scope.test = 0;
});
I have tried a multitude of things to use in a unit test to correctly test the functionality of this blur event however I have not been successful. I have seen mention of the function triggerHandler. Here is my attempt at the unit test:
//Inject $compile and $rootScope before each test.
beforeEach(inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
$scope.test = 5
html = angular.element('<input type="text" data-type-ahead/>');
//Apply $scope to directive html.
directive = $compile(html)($scope);
//Trigger digest cycle.
$scope.$digest();
}));
it('should trigger the events bound to the blur event on the directive', function() {
html.triggerHandler('blur')
expect($scope.test).toEqual(0);
});
However this is failing because $scope.test is remaining on 5. Is it the html element is incorrect, do I need another $digest or $apply for after I trigger the event?
You have 2 ways of getting this to work. The first is adding a timeout to your method (docs):
// somewhere above add -
var originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
// in your beforeEach
beforeEach(function(){
html.triggerHandler('blur');
setTimeout(function(){
done();
}, 1000);
});
it('should trigger the events bound to the blur event on the directive',
function() {
expect($scope.test).toEqual(0);
}
);
I believe this to be "less good" practice (bad or worse is too negative a word for testing - the moment you test, you are already better :)). In general, I try to avoid testing async, because eventually my methods (a.k.a. units) are sync inside.
The "better practice" would be to write the method that changes the value like this:
// in the directive's ctrl
this.changeValue = function changeValue{
$scope.test = 0;
}
// later on set the watcher
// Bind blur event and clear the value of
$element.bind('blur', this.changeValue);
And then test the method itself instead of testing it async. You could test the $element.bind (via the spyOn(element, 'bind')) if you like to see that your ctrl/link methods create the binding.

Cannot get the scope os a controller in anugular.js

I am trying to test a javascript file which has in it a controller and some HTML DOM elements which it interacts with.
The class under test is:
function BaseConceptMenu(options) {
var baseConceptMenu = new BaseMenu(options);
//Public function -->Method under Test
function showCodeConceptToolbar() {
var scope = angular.element('#toolbar').scope();
scope.$apply(function() {
scope.toolbarController.show(baseConceptMenu.someObject);
});
}
I am trying to mock the controller and create the HTML DOM element "toolbar" on the fly without trying to create an external HTML template just for the sake of testing.
I am trying to create the div "toolbar" inside the before each and mocking the "CodeConceptToolbarController" controller
beforeEach(inject(function ($rootScope, $compile) {
elm = document.createElement('div');
elm.id = 'toolbar';
scope = $rootScope.$new();
createController = function() {
return $controller('CodeConceptToolbarController', {
$scope: scope
});
};
$compile(elm)(scope);
scope.$digest();
}));
However when I try to test it as below
it('Test Code ConeptToolbarContoller', function() {
// var toolbar = angular.element('#toolbar');
document.getElementById("toolbar").scope().toolbarController = createController();
//As of now,not doing any-idepth testing
//Just a base test call
var menu = new BaseConceptMenu({});
expect(menu.show()).toBe(true);
});
I get this error
TypeError: Cannot read property 'scope' of null
Could anyone provide a way to test this?
or is there a better way to test this?
currently I am using Maven-jasmine plugin
Two problems:
As per https://developer.mozilla.org/en-US/docs/Web/API/document.getElementById, "Elements not in the document are not searched by getElementById." $compile doesn't insert the element into the DOM - it just sets up appropriate bindings (and does smart things like handling nested directives inside your template string). Your getElementById will fail to find a match. You need to insert the element into the DOM somewhere.
getElementById returns a raw HTML DOM element. To get the Angular scope from it, the docs call for wrapping it in angular.element():
var element = document.getElementById(id);
angular.element(element).scope()...
This pattern will provide the Angular wrapper around the element to do the rest of the magic. It's all based on jqLite, which isn't jQuery but does follow a lot of the same patterns. For those used to jQuery, think of it like writing $(element).

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.

How to unit test a key press event in angular directive

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);
}));

Resources