Jamsine throws error despite same code working in browser - angularjs

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.

Related

Unit testing angular.extend function with jasmine spyOn

I have a directive, where, in certain case I use
angular.extend(dist, src)
Now I would like to test this case and check, if angular.extend is called.
I'm trying to use spyOn
spyOn(angular, 'extend')
And then in test
expect(angular.extend).toHaveBeenCalled()
Not sure I can do it at all, but I decided to give it a try.
Thanks for any help.
EDIT:
Here is my test, edited in accordance with your advise.
it('should create new scope and extend config if config is passed to directive', function() {
var spy = jasmine.createSpy('extendSpy').and.callThrough();
angular.extend = spy;
timeout.flush();
_.forEach(scope.accordionConfig, function(configItem) {
if (configItem.config) {
expect(angular.extend).toHaveBeenCalled();
}
});
});
In beforeEach hook I don't have anything special, just assigning config, creating some other preparation for rest tests and compiling the directive.
Here is a snippet from link function which I'm trying to test
if (scope.format === 'directive') {
if (scope.config) {
newScope = $rootScope.$new();
angular.extend(newScope, scope.config);
}
scope.content = $compile(scope.content)(newScope || scope);
}
console log the value of angular.extend before the assertion, it should be an instance of jasmine.spy, if it is not, there is a problem in the way that you create the spy, and we will need more context, perhaps your full code could help.
I assume you create the hook somewhere in the beforeEach hook or on one of the other hooks?
Try the following code:
var spy = jasmine.createSpy('extendSpy').and.callThrough();
angular.extend = spy;
expect(spy).toBeCalledWith(dist, src);

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 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 should I access an element's angularjs $ngModelController in a jasmine unit test?

I'm currently using directiveElement.data("$ngModelController") to get access to the element's $ngModelController, as in the following example.
describe("directiveElement", function () {
it("should do something with ngModelController", inject(function($compile, $rootScope) {
var directiveElement = $compile("<input ng-model="myNgModel" customDirective type="text"></input>")($rootScope);
$rootScope.$digest();
var ngModelCtrl = directiveElement.data("$ngModelController");
ngModelCtrl.$modelValue = "12345";
// do rest of test
}));
});
However, I want to know if there is a better to access the $ngModelController, or if accessing the $ngModelController is a bad idea?
You could also do directiveElement.controller('ngModel').
I certainly think there are legitimate testing reasons why you would want a handle on this, though the more common way is to get a handle on it through the form (eg. https://github.com/angular/angular.js/blob/master/test/ng/directive/formSpec.js)

AngularJS: Accessing scope in E2E test

I'm trying to access $scope's within an E2E test without success...
As a test I tried this: (My site does not use JQuery..)
The runner has my site in a nested iframe, so I'm accessing it directly, then getting all ng-scopes and trying .scope() on them as in this post and code below...
var frameDocument = document.getElementById('test-frames').children[0].contentDocument;
var scopeElements = frameDocument.getElementsByClassName('ng-scope');
var scopes = [].map.call(scopeElements, function (e) {
return angular.element(e).scope();
});
The above code finds the proper elements, but calling scope() on them returns undefined for each....
Can someone confirm or deny that we can access the scope in E2E? I'd assume there is a way?
Thank-you
Here is my trick based on previous answer.
You can extend it to dynamic scopes. The main part is getting the reference to appWindow from addFutureAction.
//HTML CODE
<body id="main-controller" ng-controller="mainCtrl" ng-init="__init__()">
//Scenario helper.
/*
Run `callback` with scope from `selector`.
*/
angular.scenario.dsl('scope', function() {
return function(selector, callback) {
return this.addFutureAction(
'Executing scope at ' + selector,
function(appWindow, $document, done) {
var body = appWindow.document.getElementById(selector)
var scope = appWindow.angular.element(body).scope()
callback(scope)
scope.$apply()
done(null, 'OK');
})
}
})
//Actual test.
it(
'When alerts are defined, they are displayed.',
function() {
scope('main-controller', function(scope) {
scope.alerts.push({'type': 'error', 'msg': 'Some error.'})
})
expect(element('#alerts').css('display')).toEqual('block')
})
In E2E tests, accessing scope that way is not good option. Instead You can use helper functions like element() to select elements in page, and use expect() to check model data.
What you might need is unit testing. You can access $scope in unit tests easily.
There is a very good guide here: http://www.yearofmoo.com/2013/01/full-spectrum-testing-with-angularjs-and-testacular.html
Also it might be a timing issue, i can reach scopes in testacular runner like this. It runs tests in iframe. To make it work you need to add sleep(3) to your test. But this is very fragile.
setTimeout(function () {
console.log('try to reach frames');
var frame = window.frames[0].window.frames['senario_frame'];
if (!frame) {
console.log('too late');
} else {
var scopeElements = frame.document.getElementsByClassName('ng-scope');
var scopes = [].map.call(scopeElements, function (e) {
return frame.angular.element(e).scope();
});
console.log(scopes);
}
}, 2000);

Resources