I'm writing tests for a directive. I want pass in a templateURL and then test various functionality of it using the built in JQlite library.
For some reason my template doesn't seem to compile, though Jasmine doesn't give me any errors.
my relevant app structure is:
/app
../partials/
../tests/
I have my karma.conf setup so:
files: [
...
'tests/*.js',
'partials/*.html'
],
preprocessors: {
'partials/*.html':'ng-html2js'
},
plugins: [
'karma-chrome-launcher',
'karma-jasmine',
'karma-ng-html2js-preprocessor',
'karma
]
My test code looks like this:
describe('mapInputs.customForm', function () {
var elm, scope;
beforeEach(module('myApp'));
beforeEach(module('partials/test/tmpl.html'));
beforeEach(inject( function (_$rootScope_, _$compile_) {
$rootScope = _$rootScope_;
$compile = _$compile_;
elm = angular.element('<div><ng-map-inputs map-inputs=""></ng-map-inputs></div>');
console.log('elm', elm);
elm = $compile(elm)($rootScope);
$rootScope.$digest();
console.log('elm', elm);
}));
it('should reveal custom form', function () {
// elm.find('div') returns an empty object
});
});
And the output:
LOG: 'elm', Object{length: 1, 0: <div><ng-map-inputs map-inputs=""></ng-map-inputs></div>}
LOG: 'elm', Object{length: 1, 0: <div class="ng-scope"><ng-map-inputs map-inputs=""></ng-map-inputs></div>}
As I said, I don't get any errors, but the output of the console logs is not what I expect. It seems like the template is not being compiled? But surely Jasmine would complain if no template was found?
The issue for me was that I wasn't loading the actual directive in the karma.conf.js:
files: [
...
'path/to/directive/map-inputs.directive.js', // <-- wasn't being loaded before
'tests/*.js',
'partials/*.html'
],
Related
So, I started angular this weekend and I wrote myself a nice carousel app to begin with, good fun!
I wrote a neat directive for the carousel making it easy to use for whoever wants to install my carousel app. Unfortunatly I am getting stuck on retrieving the scope of the directive containing the functions I create there (I want to test them).
My karma.config.js contains the following related lines of code:
...
files: [
// My templates
'javascript/vendor/Directives/carousel/carousel.html',
]
...
...
preprocessors: {
'javascript/vendor/Directives/carousel/carousel.html' : ['ng-html2js']
},
...
...
ngHtml2JsPreprocessor: {
moduleName: 'myDirectives'
},
...
...
// Which plugins to enable
plugins: [
'karma-jasmine',
'karma-chrome-launcher',
'karma-ng-html2js-preprocessor'
],
...
In my carouselDirective.js this is the relevant code:
angular.module('carouselApp').directive('carousel', function() {
return {
restrict : 'E',
templateUrl : 'javascript/vendor/Directives/carousel/carousel.html',
controller : controller,
scope : {
images : '=',
theme : '='
}
};
function controller ($scope, $interval) {
...
$scope.resetInterval = function () {
$interval.cancel(carouselInterval);
return carouselInterval = $interval(function() {
$scope.nextImage();
}, 5000);
};
...
};
And finally in my controllerSpec.js
describe("Unit: Testing carouselApp - ", function() {
var scope = undefined,
ctrl = undefined;
beforeEach(module('carouselApp'));
beforeEach(module('myDirectives'));
beforeEach(
inject(function($rootScope, $controller, $injector, $compile) { //injects the dependencies
var $carouselService = $injector.get('carouselService'),
carouselElement = angular.element('<carousel images="images" theme="theme"></carousel>');
scope = $rootScope.$new();
ctrl = $controller('carouselController', {
$scope : scope,
theme : 'fantasy',
images : $carouselService.getFantasyImages()
});
$compile(carouselElement)(scope);
scope.$digest();
// THIS HERE IS THE PROBLEM:
// the first parameter in the log contains the scope I created from $rootScope and the isolateScope is simply undefined.
// How can I get the scope from the directive containing the resetInterval function?
console.log(carouselElement.scope(), carouselElement.isolateScope());
})
);
});
This is the situation, for those that skipped reading the code.. I added the problem I am encountering in the comments of the controllerSpec.js!
Any help is much appreciated. I have scoured google and stackoverflow.. I am not saying the answer to my problem is not there, but if it is I may need a bit more of a hand to understand whats going on :)
Regards,
A new angular enthusiast!
Yikes, it turned out.. the code I had was fine. The instance of karma in my terminal seems to have bugged out somehow. Closing the terminal and opening a whole new one somehow solved the problem!
Partially happy it seems like I'm getting the hang of this, but that was a big chunk of my nights rest just for that! haha :)
Thanks for those that pondered upon the question!
In my karma.conf.coffee, I have:
files: [
'public/bower_components/angular-mocks/angular-mocks.js'
'public/scripts/**/*.coffee' # not tested since configuration is difficult to be tested and perhaps should not be tested
'test/webapp/unit/**/*.coffee'
'views/templates/*.html'
]
preprocessors:
'public/scripts/**/*.coffee': ['coverage']
'test/webapp/unit/**/*.coffee': ['coffee']
'views/templates/*.html': ['ng-html2js']
frameworks: ['jasmine']
ngHtml2JsPreprocessor:
stripPrefix: 'views/'
In my test, I have:
describe('Directive: timespanSelector', function() {
var scope;
scope = null;
beforeEach(module('myApp'));
beforeEach(module('templates/partialDateRange.html'));
beforeEach(function() {
var html;
html = "<timespan-selector></timespan-selector>";
return inject(function($compile, $rootScope) {
var compiled, elem;
scope = $rootScope.$new();
elem = angular.element(html);
compiled = $compile(elem);
compiled(scope);
return scope.$digest();
});
});
return it('should test', function() {});
});
When I run it, it says:
Error: [$injector:modulerr] Failed to instantiate module templates/partialDateRange.html due to:
Error: [$injector:nomod] Module 'templates/partialDateRange.html' is not available! You either misspelled the module name or forgot to load it. If registering a module ensure that you specify the dependencies as the second argument.
http://errors.angularjs.org/1.2.16/$injector/nomod?p0=templates%2FpartialDateRange.html
What am I doing wrong?
This has been an issue for us for a long time and I spent a few days to find out the solution - this it what we came up with. I will simply show you how our file structure is configured.
First off you will need to include karma-ng-html2js-preprocessor.
npm install karma-ng-html2js-preprocessor --save-dev
Next your karma.conf.js - you use coffee but i wont hold it against you ;-)
Remember to include the module name so you can inject it into the directive unit test.
// Karma configuration
// http://karma-runner.github.io/0.10/config/configuration-file.html
module.exports = function (config) {
config.set({
// base path, that will be used to resolve files and exclude
basePath: '',
// testing framework to use (jasmine/mocha/qunit/...)
frameworks: ['jasmine'],
preprocessors: {
'app/views/templates/*.tpl.html': ['ng-html2js'] //<----- Also needed
},
// list of files / patterns to load in the browser
files: [
'app/bower_components/angular/angular.js',
'app/bower_components/angular-mocks/angular-mocks.js',
'app/bower_components/angular-resource/angular-resource.js',
'app/bower_components/angular-cookies/angular-cookies.js',
'app/bower_components/angular-sanitize/angular-sanitize.js',
'app/bower_components/angular-bootstrap/ui-bootstrap.js',
'app/bower_components/angular-ui-router/release/angular-ui-router.js',
'app/bower_components/angular-local-storage/angular-local-storage.js',
'app/bower_components/jquery/dist/jquery.js',
'app/bower_components/bootstrap/dist/js/bootstrap.js',
'app/scripts/*.js',
'app/scripts/**/*.js',
'test/spec/**/*.js',
//Templates
'app/views/templates/*.tpl.html' //<----- Also needed
],
ngHtml2JsPreprocessor: {
stripPrefix: 'app/',
moduleName: 'Kinetix.Templates' //<-----Module Name for injection
},
// list of files / patterns to exclude
exclude: [],
// Reporters
reporters: ['progress', 'junit'],
//Config for junit
junitReporter: {
outputFile: './test/test-results.xml',
suite: ''
},
// web server port
port: 9001,
// level of logging
// possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: false,
// Start these browsers, currently available:
// - Chrome
// - ChromeCanary
// - Firefox
// - Opera
// - Safari (only Mac)
// - PhantomJS
// - IE (only Windows)
browsers: ['PhantomJS'],
// Continuous Integration mode
// if true, it capture browsers, run tests and exit
singleRun: false
});
};
So then after you got yer karma.conf.js set up lets have a look at a unit test. This is a directive unit test I will include the whole thing as well - maybe it can inspire your for other unit testing ideas as well. I have been at it now for almost 8 months and learned quite a bit...
Directive Unit Test:
'use strict';
describe('Directives: Search', function () {
var//iable declarations
elm,
scope,
$rootScope,
$compile,
$animate,
ACCESS_LEVEL = [
'OPEN',
'PRIVATE',
'RESTRICTED'
]
;
beforeEach(function () {
module('Kinetix.Constants');
module('Kinetix.Templates'); //<------ See here we inject the templates!
module('Kinetix.Directives.Search');
module('Kinetix.Controllers.Search', function ($controllerProvider) {
$controllerProvider.register('SearchController', function () { });
});
});
beforeEach(inject(function (_$rootScope_, _$compile_, _$animate_) {
$rootScope = _$rootScope_;
scope = $rootScope;
$animate = _$animate_;
$compile = _$compile_;
}));
function setupDirective(accessLevel) {
spyOn($animate, 'addClass').and.callThrough();
spyOn($animate, 'removeClass').and.callThrough();
$rootScope.accessLevel = { type: accessLevel };
$rootScope.isAuthenticated = { value: false };
elm = angular.element('<kx-search></kx-search>');
$compile(elm)(scope);
scope.$apply();
}
it('Should create the search template', function () {
setupDirective(ACCESS_LEVEL[0]);
var nav = $(elm).find('.nav');
expect(nav.children('form')).toBeTruthy();
});
describe('Animations', function () {
it('should have the fade slide class on setup with OPEN accesslevel', function () {
setupDirective(ACCESS_LEVEL[0]);
//With Authentication
$rootScope.isAuthenticated.value = true;
scope.$apply();
expect(elm.children('div').hasClass('slide-left')).toBeTruthy();
expect($animate.addClass).toHaveBeenCalled();
$rootScope.isAuthenticated.value = false;
scope.$apply();
expect($animate.removeClass).toHaveBeenCalled();
expect(elm.children('div').hasClass('slide-left')).toBeFalsy();
});
it('should toggle the fade-slide animation with PRIVATE acesslevels', function () {
setupDirective(ACCESS_LEVEL[1]);
expect($animate.addClass).toHaveBeenCalled();
expect(elm.children('div').hasClass('fade-slide')).toBeTruthy();
$rootScope.isAuthenticated.value = true;
scope.$apply();
expect($animate.removeClass).toHaveBeenCalled();
expect(elm.children('div').hasClass('fade-slide')).toBeFalsy();
$rootScope.isAuthenticated.value = false;
scope.$apply();
expect($animate.addClass).toHaveBeenCalled();
expect(elm.children('div').hasClass('fade-slide')).toBeTruthy();
});
it('should toggle the fade-slide animation with RESTRICTED acesslevels', function () {
setupDirective(ACCESS_LEVEL[2]);
expect($animate.addClass).toHaveBeenCalled();
expect(elm.children('div').hasClass('fade-slide')).toBeTruthy();
$rootScope.isAuthenticated.value = true;
scope.$apply();
expect($animate.removeClass).toHaveBeenCalled();
expect(elm.children('div').hasClass('fade-slide')).toBeFalsy();
$rootScope.isAuthenticated.value = false;
scope.$apply();
expect($animate.addClass).toHaveBeenCalled();
expect(elm.children('div').hasClass('fade-slide')).toBeTruthy();
});
});
});
For completion I will also include the directive itself just so you can see the complete picture.
angular.module('Kinetix.Directives.Search', [])
.directive('kxSearch', function ($rootScope, $animate, PATH) {
var linker = function (scope, el) {
var//iable declarations
accessLevel = $rootScope.accessLevel.type || 'Guest',
element = el.children('.nav')
;
//Check if the shop type is a PRIVATE or RESTRICTED type so we can chose which animation to apply
if (accessLevel === 'RESTRICTED' || accessLevel === 'PRIVATE') {
// Hide the element as we need authentication to show it
$animate.addClass(element, 'fade-slide');
$rootScope.$watch('isAuthenticated.value', function (newVal) {
if (!!newVal) {
$animate.removeClass(element, 'fade-slide');
}
if (!newVal) {
$animate.addClass(element, 'fade-slide');
}
});
}
if (accessLevel === 'OPEN') {
$rootScope.$watch('isAuthenticated.value', function (newVal, oldVal) {
if (newVal !== oldVal) {
if(!!newVal) {
$animate.addClass(element, 'slide-left');
}
if(!newVal) {
$animate.removeClass(element, 'slide-left');
}
}
});
}
};
return {
restrict: 'E',
link: linker,
controller: 'SearchController',
templateUrl: PATH.templates + 'search.tpl.html'
};
});
Hope it helps! This was the 800lb gorilla in the room for quite a while, once you actually tame him he is pretty sweet! Good luck!
What am I doing wrong?
Answer: You are attempting to reference an angular module that doesn't exist.
I know this because of the error that you're getting:
Module 'templates/partialDateRange.html' is not available!
Which is caused by the following line of code:
beforeEach(module('templates/partialDateRange.html'));
The Fix
Determine the location of ng-html2js's output. Which is a .js file.
Include that file inside of the files array in karma.conf.coffee
Open the .js file and look for angular.module('<module name>', []) to determine how ng-html2js is naming the module and use that module name where you currently have 'templates/partialDateRange.html' (it should be a similar path name if not the same).
An alternative to karma-ng-html2js-preprocessor is grunt-angular-templates. However, this assumes you are using Grunt (there's also a gulp version floating around).
Add grunt-angular-templates to your package.json file, then simply run grunt ngtemplates from your command line.
This will concatenate all of your templates throughout the application (the ones that are called with templateURL anyways), and put them in a templates.js file.
Then you simply include this file in your karma.conf and you're good to go.
I had a huge amount of trouble setting up karma-ng-html2js-preprocessor and as such, turning to a grunt task was the fastest and easiest way out.
Hope you get it working soon, whichever way you choose to go.
I have simple filter which depends of moment.js:
app.filter('fromNow', function() {
return function(date) {
return moment(date).fromNow();
}
});
Have can i write unit test of this in jasmine ?
EDIT:
now i have
ReferenceError: moment is not defined
when write like that:
describe("fromNow filter", function(){
var moment;
beforeEach(function(){
module('reports');
moment = jasmine.createSpy();
});
it("should output string when input string",
inject(function(fromNowFilter) {
fromNowFilter("string");
}));
})
You need to add moment.js to the testing framework.
I had the same problem and fixed adding the following line to my karma.conf.js
...files: [
....
'app/bower_components/moment/moment.js',
....
],
.....
Now I'm getting:
INFO [karma]: Karma v0.10.2 server started at `http://localhost:9876/`
INFO [launcher]: Starting browser Chrome
INFO [Chrome 30.0.1599(Linux)]: Connected on socket Q8d9RLBQDqi7wJ8iaKNw Chrome 30.0.1599 (Linux)
directives Mydirective directive should render Roi element correctly FAILED
Error: Unexpected request: GET partials/directives/Mydirective.html
No more request expected
at Error (<anonymous>)
at $httpBackend (/home/user/Documents/Projects/angularproject/test/lib/angular/angular-mocks.js:1006:9)
I'm trying to unittest a simple directives
.directive('Mydirective', function(){
return {
restrict:'E',
replace:true,
transclude:false,
templateUrl:'partials/directives/Mydirective .html'
};
})
this is karma.conf
module.exports = function(config){
config.set({
basePath : '../',
files : [
//libs, not sure I need all of them
'app/lib/angular/angular.js',
'app/lib/angular-file-upload/**/*.js',
'app/lib/restangular/restangular.js',
'app/lib/jquery/jquery.js',
'app/lib/angular/**/*.js',
'app/lib/lodash/**/*.js',
'app/lib/Underscore.js',
'app/lib/bootstrap.min.js',
'app/lib/ui-bootstrap-0.6.0.js',
'app/lib/angular/angular-resource.js',
'app/lib/angular/angular-cookies.min.js',
'app/lib/angular-translate/**/*.js',
'app/lib/angular-translate-loader-static-files/**/*.js',
'app/lib/angular-translate-storage-cookie/**/*.js',
'app/lib/angulartestfile-translate-storage-local/**/*.js',
'app/lib/bootstrap-gh-pages/assets/rainbow.js',
'app/lib/fbAngular.js',
'app/lib/moment.js',
// test libs
'test/lib/angular/**/*.js',
'test/lib/**/*.js',
// my js to test
'app/js/**/*.js',
//test unit
'test/unit/**/*.js',
//directive templates
'app/partials/directives/**/*.html'
],
exclude: ['test/lib/angular/angular-scenario.js'],
autoWatch : true,
testfile
frameworks: ['jasmine'],
browsers : ['Chrome'],
plugins : [
'karma-junit-reporter',
'karma-chrome-launcher',
'karma-firehttp://localhost:9876/fox-launcher',
'karma-jasmine',
'karma-coverage',
//'karma-html2js-preprocessor',
'karma-ng-html2js-preprocessor'
],
junitReporter : {
outputFile: 'test_out/unit.xml',
suite: 'unit'
},
reporters:['coverage','progress'],
preprocessors : {
'**/js/*.js': 'coverage',
'**/partials/directives/*.html': 'ng-html2js'// generate js files from html templates
},
ngHtml2JsPreprotestfilecessor: {
// cacheIdFromPath : function(filepath) {
// return filepath.substr(filepath.indexOf("angular")+8);
// },
stripPrefix:'app/',
// setting this option will create only a single module that contains templates
// from all the files, so you can load them all with module('templates')
moduleName: 'st-templates'
},
This is the test file.
describe('directives', function() {
var $compile, $rootScope;
beforeEach(function(){
module('myModule.directives');
module(function($provide) {
$provide.value('version', 'TEST_VER');
$provide.value('somevalue','value');
}
angular.module('st-templates');
});
beforeEach(inject(function(_$compile_, _$rootScope_){
// The injector unwraps the underscores (_) from around the parameter names when matching
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
describe('app-version directive', function() {
it('should print current version', function() {
// module(function($provide) {
// $provide.value('version', 'TEST_VER');
var element = $compile('<span app-version></span>')($rootScope);
$rootScope.$digest();
expect(element.text()).toEqual('TEST_VER');
});
});
describe('mydirective', function(){
it('should render Roi element correctly', function(){
var element = $compile('<st-roi></st-roi>')($rootScope);
$rootScope.$digest();
expect(element.text()).toEqual('0.3%');
});
});
});
This is the folder tree:
-app
--app/js
--app/partials
-config/karma.conf
-test
--test/unit/specfile.js
Following advice in different SO question I already enabled the nghtml2j preprocessor and configured it (Hopefully correctly). But I still get an annoying error
Any clues? I'll be glad for any help with this
angular.module('st-templates') is not the same as module('st-templates') inside that jasmine test. Change angular.module('st-templates') to module('st-templates') and it should work.
I am trying to write unit tests for my Angular.js application but I cannot manage to inject what I need (it is not able to find a suitable provider).
Does anyone see what I missed?
Firefox 21.0 (Linux) filter staticList should convert static list object into its display value FAILED
Error: Unknown provider: staticListProvider <- staticList in /path/to/my-app/public/third-party/angular/angular.js (line 2734)
createInjector/providerInjector<#/path/to/my-app/public/third-party/angular/angular.js:2734
getService#/path/to/my-app/public/third-party/angular/angular.js:2862
createInjector/instanceCache.$injector<#/path/to/my-app/public/third-party/angular/angular.js:2739
getService#/path/to/my-app/public/third-party/angular/angular.js:2862
invoke#/path/to/my-app/public/third-party/angular/angular.js:2880
workFn#/path/to/my-app/test/lib/angular/angular-mocks.js:1778
angular.mock.inject#/path/to/my-app/test/lib/angular/angular-mocks.js:1764
#/path/to/my-app/test/unit/filtersSpec.js:19
#/path/to/my-app/test/unit/filtersSpec.js:16
#/path/to/my-app/test/unit/filtersSpec.js:3
The application:
angular.module('myApp', ['myAppFilters', 'ui.bootstrap', '$strap.directives']).
// Some other stuff
The filters:
"use strict";
angular.module('myAppFilters', []).
filter('staticList', function () {
return function (listItem) {
if (!listItem) {
return '';
}
return listItem.value;
};
});
The test:
'use strict';
describe('filter', function () {
beforeEach(angular.module('myAppFilters'));
describe('staticList', function () {
it('should convert static list object into its display value',
inject(function (staticList) {
expect(undefined).toBe('');
expect({key: 'A', value: 'B'}).toBe('B');
}));
});
});
The Karma configuration:
basePath = '../';
files = [
JASMINE,
JASMINE_ADAPTER,
'public/third-party/jquery/*.js',
'public/third-party/angular/angular.js',
'public/third-party/angular/i18n/angular-*.js',
'public/third-party/moment/moment.min.js',
'public/third-party/moment/moment-*.js',
'public/js/**/*.js',
'test/lib/**/*.js',
'test/unit/**/*.js'
];
colors = true;
autoWatch = true;
browsers = ['Firefox'];
junitReporter = {
outputFile: 'test_out/unit.xml',
suite: 'unit'
};
If anybody wants to see the full code, the application repository is here: https://github.com/adericbourg/GestionCourrier
Thanks a lot,
Alban
In your inject code
it('should convert static list object into its display value',
inject(function (staticList) {
expect(undefined).toBe('');
expect({key: 'A', value: 'B'}).toBe('B');
}));
replace "inject(function (staticList)" with "inject(function (staticListFilter)". This is some random convention angular is following. You can check comments on this page for more info http://docs.angularjs.org/tutorial/step_09
I faced similar problem, but in my case my filter name contained suffix 'Filter' which caused the same injection problem.
.filter('assetLabelFilter', function(){
return function(assets, selectedLabels){
// Implementation here
};
});
I was finally able to solve the problem by manually injecting the filter to my test
'use strict';
describe('assetLabelFilter', function() {
beforeEach(module('filters.labels'));
var asset1 = {labels: ['A']};
var asset2 = {labels: ['B']};
var asset3 = {labels: []};
var asset4 = {labels: ['A', 'B']};
var assets = [asset1, asset2, asset3, asset4];
var assetLabelFilter;
beforeEach(inject(function($filter) {
assetLabelFilter = $filter('assetLabelFilter');
}));
it('should return only assets with selected label', function() {
var selectedLabels = ['B'];
expect(assetLabelFilter(assets, selectedLabels)).toEqual([asset2, asset4]);
});
});
The nice answer above made me realize that in order to use the angular tutorial way:
it('should ', inject(function(assetLabelFilter) {
var selectedLabels = ['B'];
expect(assetLabelFilter(assets, selectedLabels)).toEqual([asset2, asset4]);
}));
The filter name can't have the suffix 'Filter' because it is magic word as mentioned in the answer above.
To demonstrate the scenario I also created a Plunker