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

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.

Related

Expose object fron Angularjs App to Protractor test

I am writing end-to-end tests for my AngularJS-based application using Protractor. Some cases require using mocks to test - for example, a network connection issue. If an AJAX request to server fails, the user must see a warning message.
My mocks are registered in the application as services. I want them to be accessible to the tests to write something like this:
var proxy;
beforeEach(function() { proxy = getProxyMock(); });
it("When network is OK, request succeeds", function(done) {
proxy.networkAvailable = true;
element(by.id('loginButton')).click().then(function() {
expect(element(by.id('error')).count()).toEqual(0);
done();
});
});
it("When network is faulty, message is displayed", function(done) {
proxy.networkAvailable = false;
element(by.id('loginButton')).click().then(function() {
expect(element(by.id('error')).count()).toEqual(1);
done();
});
});
How do I implement the getProxyMock function to pass an object from the application to the test? I can store proxies in the window object of the app, but still do not know how to access it.
After some reading and understanding the testing process a bit better, it turned to be impossible. The tests are executed in NodeJS, and the frontend code in a browser - Javascript object instances cannot be truly shared between two different processes.
However, there is a workaround: you can execute a script inside browser.
First, your frontend code must provide some sort of service locator, like this:
angular.module('myModule', [])
.service('proxy', NetworkProxy)
.run(function(proxy) {
window.MY_SERVICES = {
proxy: proxy,
};
});
Then, the test goes like this:
it("Testing the script", function(done) {
browser.executeScript(function() {
window.MY_SERVICES.proxy.networkAvailable = false;
});
element(by.id('loginButton')).click().then(function() {
expect(element.all(by.id('error')).count()).toEqual(1);
done();
});
});
Please note that when you use executeScript, the function is serialized to be sent to browser for execution. This puts some limitations worth keeping in mind: if your script function returns a value, it is a clone of the original object from browser. Updating the returned value will not modify the original! For the same reason, you cannot use closures in the function.

Testing code that uses document.body.querySelectorAll

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.

Decorate Service in Directive based on Attributes in AngularJS

Disclosure: I'm new to angular, so if I'm doing something that appears strange or just plain wrong, feel free to point that out.
I have a directive vpMap that I want to use like this:
<div my-map with-tile-layers with-geolocator></div>
I have a Map service that I want to decorate conditionally, based on the attributes of the directive that begin with with
It looks something like this:
angular.module('myApp.map',[])
.service('Map',someServiceFunction)
.directive('myMap',['Map','$provide',function(Map,$provide) {
return {
controller: function($scope,$element,$attrs) {
angular.forEach($attrs,function(val,key) {
if(key.match(/^with/)) {
switch(key) {
case 'withTileLayers':
$provide.decorator(Map,someDecoratorFunction);
break;
}
}
});
}
};
}]);
It's at this point that I discover I can't access the $provide service in my directive, although I'm not sure why. According to the documentation you can inject anything into directives and, I thought $provide was one of those global angular services like $http
Is this bad architecture? What rules am I not understanding?
Providers are meant to be used by the $injector in the configuration phase.
From the providers guide:
Once the configuration phase is over, interaction with providers is
disallowed and the process of creating services starts. We call this
part of the application life-cycle the run phase.
So you would have to use the config block for decorating.
Attempted Fix
To decorate a service conditionally, you may use some predicate inside the decorator's function, something along the lines of this (not tested):
// define a predicate that checks if an object starts with a key
function keyInObject(obj, key) {
var propKeys = Object.keys(obj),
index = propKeys && propKeys.length || 0;
while (index--) {
if (propKeys[index].indexOf(key) === 0) {
return true;
}
}
return false;
}
app.config(function($provide) {
// uhmm, decorate
$provide.decorator('Map', function($delegate) {
var service = $delegate;
// only extend the service if the predicate fulfills
keyInObject(service, 'with') && angular.extend(service, {
// whatever, man
});
return $delegate;
});
});
However, even this won't help us, as we need to get the keys in the linking phase, which is well after the service have been injected (and decorated).
To make sure you fully understand the meaning of decoration here; we use decoration to permanently alter the object (the service) structure. Decoration refers to the object's meta data, not its state.
Solution
Given the above, the best choice would be to simply create a service with all the required functionality, and abandon decoration altogether. Later, while inside link you can use the iteration to decide which of the methods to call, e.g.:
// I took the liberty of switching the positions of key/val arguments
// for semantics sake...
angular.forEach($attrs, function(key, val) {
if (val.match(/^with/)) {
switch (val) {
case 'withTileLayers':
Map.useTileLayers(); // use specific API in the service
break;
}
}
});

How do I provide re-usable sample data values to my angularjs / jasmine unit-tests

I would like to provide simple constant values such as names, emails, etc to use in my jasmine unit tests.
A similar question was asked here: AngularJS + Karma: reuse a mock service when unit testing directives or controllers
In c# I would create a static class to hold little snippets of mock data. I can then use these values throughout my unit tests, like this:
static class SampleData
{
public const string Guid = "0e3ae555-9fc7-4b89-9ea4-a8b63097c50a";
public const string Password = "3Qr}b7_N$yZ6";
public const string InvalidGuid = "[invalid-guid]";
public const string InvalidPassword = "[invalid-password]";
}
I would like to have the same convenience when testing my AngularJS app using Karma / Jasmine.
I know that I can define a constant object against my angular app, I already do this for constants I use in the real code, like this:
myApp.constant('config', {apiUrl:'http://localhost:8082'})
I could add another constant just like this but only containing sample data values for use in my unit tests, like this:
myApp.constant('sampleData', {email: 'sample#email.com'})
I could then just inject the mock constant object into my tests and off I go, like this
describe 'A sample unit test', ->
beforeEach -> module 'myApp'
beforeEach inject ($injector) ->
#sampleData = $injector.get 'sampleData'
email = #sampleData.email
# etc ...
However this seems a bit fishy to me. I don't want my production code to contain sample data that is only required by my unit-tests.
How would you conveniently provide your angular / jasmine unit tests with re-usable sample data values?
Thanks
There are two ways of doing this:
spy on function calls and return fake values.
create mock classes (and possibly mock data to initialise them) and load them wherever you need
The first one is alright when you only have to fake a few calls. doing that for a whole class is unsustainable.
For example, let's say you have a service that builds some special URLs. If one of the methods depends on absUrl, you can fake it by spying on the method in the $location object:
describe('example') function () {
beforeEach(inject(function () {
spyOn($location, 'absUrl').andCallFake(function (p) {
return 'http://localhost/app/index.html#/chickenurl';
});
}));
it('should return the url http://www.chicken.org') ... {
// starting situation
// run the function
// asserts
}
Now let's say that you have a Settings Service that encapsulates data like language, theme, special paths, background color, typeface... that is initialised using a remote call to a server.
Testing services that depend on Settings will be painful. You have to mock a big component with spyOn every time. If you have 10 services... you don't want to copypaste the spyOn functions in all of them.
ServiceA uses Settings service:
describe('ServiceA', function () {
var settings, serviceA;
beforeEach(module('myapp.mocks.settings')); // mock settings
beforeEach(module('myapp.services.serviceA')); // load the service being tested
beforeEach(inject(function (_serviceA_, _settings_) {
serviceA = _serviceA_;
settings = _settings_;
}));
container
for this test suite, all calls to the Settings service will be handled by the mock, which has the same interface as the real one, but returns dummy values.
Notice that you can load this service anywhere.
(If, by any reason, you needed to use the real implementation, you can load the real implementation before the mock and use spyOn for that particular case to delegate the call to the real implementation.)
Normally you'll place the mocks module outside of the app folder. I have a test folder with the unit tests, e2e tests and a lib folder with the angular-mocks.js file. I place my mocks there too.
Tell karma the files you need for the tests:
files: [
'app/lib/jquery/jquery-1.9.1.js',
'test/lib/jasmine-jquery.js',
'app/lib/angular/angular.js',
'app/lib/angular/angular-*.js',
'test/lib/angular/angular-mocks.js',
'test/lib/myapp/*.js', /* this is mine */
'app/js/**/*.js',
'test/unit/**/*.js'
],
The file tests/lib/myapp.mocks.settings.js looks just like any other module:
(function () {
"use strict";
var m = angular.module('myapp.mocks.settings', []);
m.service('settings', function () { ... })
})
Second problem (optional): you want to change quickly the dummy values. In the example, the settings service fetches an object from the server when it is instantiated for the first time. then, the service has getters for all fields. This is kind of a proxy to the server: instead of sending a request everytime you need a value, fetch a bunch of them and save them locally. In my application, settings don't change in the server in run-time.
Something like:
1. fetch http://example.org/api/settings
2. var localSettings = fetchedSettings;
3 getFieldA: function() { return localSettings.fieldA; }
Go and pollute the global namespace. I created a file settings.data.js with a content like:
var SETTINGS_RESPONSE = { fieldA: 42 };
the mock service uses this global variable in the factory to instantiate localSettings

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