Reputation: 1181
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.
Upvotes: 1
Views: 3145
Reputation: 38490
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.
Upvotes: 2