Testing code that uses document.body.querySelectorAll - angularjs

Trying to test a directive that does the following:
var inputs = angular.element(document.body.querySelectorAll('input[ng-model]', elem));
// [code relying on found elements]
Running inside karma/jasmine/phantomjs, this fails because it seems that document returns the document that contains the test, rather than the compiled template. Is there some way to mock this functionality so it works as expected (for my use case) or some other way to query for those elements?
PS: The elements that need to be located are in no known relation to the element that the directive is applied to.

You can use $document instead of document then mock it in your tests.
See Angular js unit test mock document to learn how to mock $document.

The last update in this answer did the trick for me he basically is using the $document service, which is like a wrapper over jQuery and then you can append elements to the body directly and test them:
I'll quote his answer:
UPDATE 2
I've managed to partially mock the $document service so you can use
the actual page document and restore everything to a valid state:
beforeEach(function() {
module('plunker');
$document = angular.element(document); // This is exactly what Angular does
$document.find('body').append('<content></content>');
var originalFind = $document.find;
$document.find = function(selector) {
if (selector === 'body') {
return originalFind.call($document, 'body').find('content');
} else {
return originalFind.call($document, selector);
}
}
module(function($provide) {
$provide.value('$document', $document);
});
});
afterEach(function() {
$document.find('body').html('');
});
Plunker: http://plnkr.co/edit/kTe4jKUnypfe6SbDECHi?p=preview
The idea is to replace the body tag with a new one that your SUT can
freely manipulate and your test can safely clear at the end of every
spec.

Related

Karma/Jasmine directive testing dom compiles but can't access it

I have been trying to figure out a way to test the focusElement function of my directive. But somehow, whenever I call the function in my test, the classElements variable is undefined. Anybody have a clue?
Here is the directive function
$scope.focusElement = function() {
if (attrs.focusToClass) {
$scope.classElements = angular.element(document.querySelectorAll('.' + attrs.focusToClass));
if (attrs.focusToClassIndex) {
// Focus to the class with the specified index.
// Index should be within the length of elements and must not be a negative number.
if (attrs.focusToClassIndex < $scope.classElements.length && attrs.focusToClassIndex >= 0) {
$scope.elementToFocus = $scope.classElements[attrs.focusToClassIndex];
}
} else {
// Goes to the first index if the index is not specified.
$scope.elementToFocus = $scope.classElements[0];
}
} else if (attrs.focusToId) {
// Focus to the given ID
$scope.elementToFocus = angular.element(document.querySelector('#' + attrs.focusToId))[0];
}
if ($scope.elementToFocus) {
$scope.elementToFocus.focus();
}
}
Here is the unit test code.
describe('focusElement function', function () {
var targetElement;
var element;
var elementScope;
var elementToFocus;
beforeEach(function() {
targetElement = $compile('<div id="targetDiv" class="targetClass"><span id="targetSpan" class="targetClass"></span></div>')($rootScope);
$rootScope.$apply();
});
it('should return the class element with index 0', function() {
element = $compile('<div next-focus focus-to-class="targetClass"></div>')($rootScope);
});
it('should return the class element with the specific index within the range', function() {
element = $compile('<div next-focus focus-to-class="targetClass" focus-to-class-index="1"></div>')($rootScope);
});
it('should return the class element with the specific index outside the range', function() {
element = $compile('<div next-focus focus-to-class="targetClass" focus-to-class-index="-1"></div>')($rootScope);
});
it('should return the id element', function() {
element = $compile('<div next-focus focus-to-id="targetDiv"></div>')($rootScope);
});
afterEach(function() {
elementScope = element.scope();
spyOn(elementScope, 'focusElement').and.callThrough();
elementScope.focusElement();
console.log(elementScope.classElements);
expect(elementScope.focusElement).toHaveBeenCalled();
expect(elementScope.elementToFocus).toBeDefined();
expect(elementScope.elementToFocus.focus).toHaveBeenCalled();
});
});
Here is the error
The error is the result of you using document directly in the code which comprises the unit under test. The solution is to refactor your code to not use document directly, rather use jQuery style $() syntax to get much the same behaviour, make the context which $() operates an injectable dependency and then in your unit test use things like fixtures to inject a well-known context during test. Since you are using Jasmine already, you probably want to look into jasmine-jquery for convenient API to do this easily.
(Alternatively, in this specific case you could also setup a stub/mock document.querySelectorAll in your beforeEach() callback.)
That's the where-does-the-issue-stem-from and the how-to-fix-it (high level) but it pays to understand Karma a bit better before proceeding.
Skipping over lots of finer points, basically karma consists of three things combined into one single application:
An extensible 'dummy' HTTP server to serve up content (as configured in files). This server is structured as an Express JS app, which is useful to remember if you ever want to integrate custom middleware. E.g. to expose additional paths on the server in order to, say, provide a dummy API server for your Angular app code to interact with. A particular path to remember is '/base' which corresponds to the project directory as defined in your Karma config (the working directory).
A driver to point a browser to a synthesied dummy 'index.html' kind of page on the HTTP server (which is how all entries in files for which included: true are loaded, basically as <script> tags).
An API/framework for integrating unit test logic, reporting, etc. This is where the karma-jasmine type plugins interact and also how Karma is able to get the output back out and determine whether tests succeeded or not.
Points #2 and #3 have implications, in particular that the environment (window) and the HTML (document) should essentially be treated as an internal implementation detail of Karma and are not to be relied on during tests. So that means your code under test also should not rely on these details either. This is why you should restructure your code to pass in the context for $() as a dependency and then you can pass a well-known fixture in your tests and the normal context in your actual app.

Testing a filter used within a controller function

I have the following test case:
it('should return id if the post is successful',function(){
var result = {
id : "123"
};
ctrl.saveCallback(result);
expect(ctrl.method.id).to.equal("123");
});
Where ctrl.saveCallback copies the result.id into the method.id on the ctrl and then shows the success banner. On the success banner, we are using the translate filter to translate the message before showing it.
Function:
.....
ctrl.method.id = result.id;
magicallyShowOnScreen($filter('translate')('MESSAGES.SUCCESS'));
....
magicallyShowOnScreen is a service that shows whatever string we pass onto the screen, and that has been injected into beforeEach.
Can someone please point in the right direction as to how should I test or mock out this $filter('translate') ?
Preface: I'm not familiar with Mocha.js or how one would create spies however you would still inject them or similar mock objects the same way as you would for other testing frameworks.
Below is a Jasmine example, I hope it helps.
When you bootstrap your module (using the module / angular.mocks.module) function, you should provide your own version of $filter which should be a mock / spy that returns another mock / spy. For example
var $filter, filterFn;
beforeEach(module('myModule', function($provide) {
$filter = jasmine.createSpy('$filter');
filterFn = jasmine.createSpy('filterFn');
$filter.and.returnValue(filterFn);
$provide.value('$filter', $filter);
});
Then, in your test, you can make sure $filter is called with the right arguments, eg
expect($filter).toHaveBeenCalledWith('translate');
expect(filterFn).toHaveBeenCalledWith('MESSAGE.SUCCESS');
expect(magicallyShowOnScreen).toHaveBeenCalled(); // assuming this too is a spy

How to avoid code duplication in AngularJS Jasmine tests

I'm part of a team that's been working on angularjs for quite a while now and our unit tests consist of files which contain both the test logic and provider mocks of each component that tested element relies on.
This is leading to more and more code duplication, can anyone recommend an angular-ey method whereby we could maintain a test infrastructure, mock a controller or service once and be able to inject that mocked service into other tests as required?
Edit The code I'm working with is proprietary but for an example consider you have 10 or 15 services which retrieve data via HTTP requests, format the data in different ways and return that data. One example might return arrays of data so in each file which relies on the service we would have initialising code like the following ( cut down and altered so please ignore any typos or other mistakes )
myDataService: {
getDataParsedLikeY: {
[{obj1},{obj2}...]
},
getDataParsedLikeX: {
[{obja},{objb}...]
}
beforeEach(angular.mock.module('myModule'));
beforeEach(angular.mock.inject(function(myDataService) {
myDataService = function( functionName ) {
return myDataService[functionName];
}
spyOn(myDataService).and.callThrough();
})
}
If you are looking for a way not to declare same mock codes in every test files, I would suggest something like below although I'm not sure this is angular-way or not.
[1] write the code to declare your mock services like below only for unit test and import after angular-mocks.js
In your-common-mocks.js(you can name the file as you like)
(function(window){
window.mymock = {};
window.mymock.prepare_mocks = function(){
module(function($provide) {
$provide.service('myDataService', function(){
this.getDataParsedLikeY = function(){
return 'your mock value';
};
});
});
};
})(window);
If you use karma, you could write like this to import the file in karma.conf.js.
files: [
...
'(somedirctory)/angular-mocks.js',
'(somedirctory)/your-common-mocks.js'
...
]
[2] use mymock.prepare_mocks function in your test files.
describe('Some Test', function() {
beforeEach(mymock.prepare_mocks);
it('getDataParsedLikeY', inject(function(myDataService){
expect('your mock value', myDataService.getDataParsedLikeY());
As a result of above, you have to write your mock code just one time and share the mock code in your every test files. I hope this could suggest you something to achieve what you want.
If your jasmine version is 2.x, you can write test code like below without writing mock service.
spyOn(YourService, 'somemethod').and.returnValue('mock value');
In jasmine 1.3 you need to adjust little bit.
spyOn(YourService, 'somemethod').andReturn('mock value');
I hope this could help you.

Backbone, Marionette, Jasmine: How to test jQuery deferred event

I'm very new to Jasmine and Marionette and looking for some help on how to test and even just the proper way to think about testing my application. Any pointers are welcome.
I have a Marionette Controller that I use to fetch my model, instantiate my views and render them. I use a method found at the bottom of this page so that the model is fetched before the view is rendered: https://github.com/marionettejs/backbone.marionette/blob/master/upgradeGuide.md#marionetteasync-is-no-longer-supported.
My controller method to fetch the model and display the view looks like so:
showCaseById: function(id){
App.models.Case = new caseModel({ id: id });
var promise = App.models.Case.fetch();
$.when(promise).then(_.bind(this.showContentView, this));
},
As you can see, it calls the showContentView after the model is fetched. That method is here:
showContentView: function(model){
App.views.Body = new bodyView({
model: App.models.Case
});
App.views.Body.on('case:update', this.submitCase, this);
// this.layout is defined in the controller's initialize function
this.layout.content.show(App.views.Body);
},
What is the proper way to test this functionality? I'd like to test the calling of the showContentView function after the completion of the promise. How should I break up the specs for this?
Thanks.
First, spy on your showContentView method and assert it has been called:
it('showCaseById', function (done) {
var controller = new Controller();
spyOn(controller, 'showContentView');
controller.showCaseById('foo');
expect(controller.showContentView).toHaveBeenCalledWith(jasmine.any(caseModel));
});
Secondly, I would recommend you stub out the call to fetch() so you don't hit the network, but it's starting to get a bit hairy now:
function caseModel() {
this.fetch = function () {
// return a promise here that resolves to a known value, e.g. 'blah'
};
}
Now, you can have a slightly stronger assertion, but this is a bit shonky because you're fiddling around with internals of your dependencies:
expect(controller.showContentView).toHaveBeenCalledWith('blah');
By overriding caseModel, when your controller method goes to create one, it gets your new version instead of the old one, and you can control the implementation of the new one just for this test.
There are ways to make this code more testable, but as it seems you're just starting out with testing I won't go into it all. You'll certainly find out those things for yourself as you do more testing.
First, it's important to understand that _.bind(fn, context) doesn't actually call fn. Instead, it returns a function that when called will call fn(). The context defines the object that fn will use internally as this.
It's not necessary but you could write showCaseById as :
showCaseById: function(id){
App.models.Case = new caseModel({ id: id });
var promise = App.models.Case.fetch();
var fn = _.bind(this.showContentView, this);
$.when(promise).then(fn);
},
As I say, that is unnecessary but now you understand that _.bind() returns a function and that $.when(promise).then(...) accepts a function as its (first) argument.
To answer the actual question, you can confirm that the App.models.Case.fetch() promise has been fulfilled by adding a further $.when(promise).then(...) statement, with a test function of your own choosing.
showCaseById: function(id){
App.models.Case = new caseModel({ id: id });
var promise = App.models.Case.fetch();
$.when(promise).then(_.bind(this.showContentView, this));
// start: test
$.when(promise).then(function() {
console.log("caseModel " + id + " is ready");//or alert() if preferred
});
// fin: test
},
The second $.when(promise).then(...) will not interfere with the first; rather, the two will execute sequentially. The console.log() satatement will provide reliable confirmation that the this.showContentView has been called successfully and that initial rendering should have happened.
If nothing is rendered at this point or subsequently, then you must suspect that this.showContentView needs to be debugged.

How can I test an AngularJS service from the console?

I have a service like:
angular.module('app').factory('ExampleService', function(){
this.f1 = function(world){
return 'Hello '+world;
}
return this;
})
I would like to test it from the JavaScript console and call the function f1() of the service.
How can I do that?
TLDR: In one line the command you are looking for:
angular.element(document.body).injector().get('serviceName')
Deep dive
AngularJS uses Dependency Injection (DI) to inject services/factories into your components,directives and other services. So what you need to do to get a service is to get the injector of AngularJS first (the injector is responsible for wiring up all the dependencies and providing them to components).
To get the injector of your app you need to grab it from an element that angular is handling. For example if your app is registered on the body element you call injector = angular.element(document.body).injector()
From the retrieved injector you can then get whatever service you like with injector.get('ServiceName')
More information on that in this answer: Can't retrieve the injector from angular
And even more here: Call AngularJS from legacy code
Another useful trick to get the $scope of a particular element.
Select the element with the DOM inspection tool of your developer tools and then run the following line ($0 is always the selected element):
angular.element($0).scope()
First of all, a modified version of your service.
a )
var app = angular.module('app',[]);
app.factory('ExampleService',function(){
return {
f1 : function(world){
return 'Hello' + world;
}
};
});
This returns an object, nothing to new here.
Now the way to get this from the console is
b )
var $inj = angular.injector(['app']);
var serv = $inj.get('ExampleService');
serv.f1("World");
c )
One of the things you were doing there earlier was to assume that the app.factory returns you the function itself or a new'ed version of it. Which is not the case. In order to get a constructor you would either have to do
app.factory('ExampleService',function(){
return function(){
this.f1 = function(world){
return 'Hello' + world;
}
};
});
This returns an ExampleService constructor which you will next have to do a 'new' on.
Or alternatively,
app.service('ExampleService',function(){
this.f1 = function(world){
return 'Hello' + world;
};
});
This returns new ExampleService() on injection.
#JustGoscha's answer is spot on, but that's a lot to type when I want access, so I added this to the bottom of my app.js. Then all I have to type is x = getSrv('$http') to get the http service.
// #if DEBUG
function getSrv(name, element) {
element = element || '*[ng-app]';
return angular.element(element).injector().get(name);
}
// #endif
It adds it to the global scope but only in debug mode. I put it inside the #if DEBUG so that I don't end up with it in the production code. I use this method to remove debug code from prouduction builds.
Angularjs Dependency Injection framework is responsible for injecting the dependancies of you app module to your controllers. This is possible through its injector.
You need to first identify the ng-app and get the associated injector.
The below query works to find your ng-app in the DOM and retrieve the injector.
angular.element('*[ng-app]').injector()
In chrome, however, you can point to target ng-app as shown below. and use the $0 hack and issue angular.element($0).injector()
Once you have the injector, get any dependency injected service as below
injector = angular.element($0).injector();
injector.get('$mdToast');

Resources