Testing AngularJS Directives: Handling External Templates

AngularJS provides support for testing directives.  Testing directives that use inline templates -- html that is embedded within the directive's code -- is a straightforward process.  What isn't straightforward is testing a directive that uses an external html template.

The Example

I have created an 'ssn' directive whose purpose is to display a user's social security number split out into three different fields, validate an ssn across all these three fields, and store the social security number as a single field internally.  The directive is as follows:

directivesModule.directive('ssn', function()
{
	return {
		restrict: 'A',
		replace: true,
		require:'ngModel',
		scope: {
			ssn: '=ngModel',
			disabled:'=ngDisabled'
		},
		//templateUrl points to an external html template.
		templateUrl: '/myApp/templates/ssnControl.htm', 
		
	//...

The file referenced in the templateUrl config option contains:

<div class="ssnContainer">
	<input type="text" tabindex="{{tabIndex1}}" name="ssn1" class="ssn1" ng-disabled="disabled"
	       placeholder="XXX"  maxlength="3" ng-model="ssn1"/>
	<input type="text" tabindex="{{tabIndex2}}" name="ssn2" class="ssn2" ng-disabled="disabled"
	       placeholder="XX"  maxlength="2" ng-model="ssn2"/>
	<input type="text" tabindex="{{tabIndex3}}" name="ssn3" class="ssn3" ng-disabled="disabled"
	       placeholder="XXXX" maxlength="4" ng-model="ssn3"/>
</div>

The above html would be cumbersome to include directly in the directive file and is correctly externalized from the directive code.  We now need to configure Angular's test framework to include the file in the testing classpath and process the file so it may be used by Angular during testing.

Configuring AngularJS to include and Pre-process the directive's external html template

//...

files = [
  JASMINE,
  JASMINE_ADAPTER,
  'main/webapp/app/lib/jquery-1.8.1.js',
  'main/webapp/app/lib/jquery.autotab-1.1b.js',
  'main/webapp/app/lib/jquery.placeholder.min.js',
  'main/webapp/app/lib/date.js',
  'main/webapp/app/lib/angular/angular.js',
  'test/js/lib/angular/angular-mocks.js',
  'main/webapp/js/**/*.js',
  'test/js/unit/**/*.js',

  //include the directory where directive templates are stored.
  'main/webapp/templates/**/*.htm'
];

// generate js files from html templates to expose them during testing.
preprocessors = {
  'main/webapp/templates/**/*.htm': 'html2js'
};

//...

The first configuration to set up is actually including the template in the test classpath.  This is done by adding an item in the files array of the karma.conf.js config file (line 16).  This will make the html file available to karma's html2js pre-processor.  the html2js pre-processor is responsible for converting HTML files into AngularJS templates.  Adding a preprocessor is done by referencing a file path and supplying the name of a pre-processor to act on that file path (lines 20 - 22).  The newly generated AngularJS template is now ready to be referenced in the directive's unit test.

Exposing the AngularJS Template to the Directive Under Test

There are 2 steps needed to expose the template to the directive:

  1. Load the template file as if it was a module (line 8)
  2. map the url of the template file in the template cache to the url the directive will use to access it during runtime (lines 13-14)

Loading a template as a module is straightforward, placing the template in the template cache bears some explanation.

The Template Cache

AngularJS maintains a cache of all html files it has converted into AngularJS templates, storing each cache in a configuration object.  The cache's config object keys are the urls of the template files as downloaded from the server.  In our situation, the url of the file during tesing will differ from the url when loaded during runtime.  If we did nothing to fix this, the template would never be found by the directive because it would be trying to load an AngularJS template using a template key (the url) that doesn't exist in the config object.  Inserting a new entry into the template cache (lines 13-14) with the url that the directive will be calling at runtime solves this problem

describe("ssnControl Directive", function() {

	var $compile, $rootScope, template;


	//load all modules, including the html template, needed to support the test
	beforeEach(module('directives',
		'main/webapp/templates/ssnControl.htm'));

	beforeEach(inject(function($templateCache,_$compile_,_$rootScope_) {

		//assign the template to the expected url called by the directive and put it in the cache
		template = $templateCache.get('main/webapp/templates/ssnControl.htm');
		$templateCache.put('/myApp/templates/ssnControl.htm',template);

		$compile = _$compile_;
		$rootScope = _$rootScope_;
	}));


	it("should display 3 text input fields, populated with ssn data", function() {

		var ssn1 = '123';
		var ssn2 = '45';
		var ssn3 = '6789';

		$rootScope.ssn = ssn1 + ssn2 + ssn3;

		//create the element angularjs will replace with the directive template
		var formElement = angular.element('<div ssn ng-model="ssn"></div>');
		var element = $compile(formElement)($rootScope);
		$rootScope.$digest();

		expect(element.find('input').length).toEqual(3);

		//use jquery to find the sub elements.
		expect($('input:first-child',element).val()).toEqual(ssn1);
		expect($('input:nth(1)',element).val()).toEqual(ssn2);
		expect($('input:nth(2)',element).val()).toEqual(ssn3);



	})

});

Now, when the directive is instantiated by the AngularJS framework, it will be able to fetch its processed template and allow you to test.

Conclusion

The ability to test AngularJS directives provides an extremely powerful tool to use during your web app's development.  Implementing the above steps will make you able to follow the best practice of maintaining external html directives from your directive's code, and at the same time be able to test directives with complex html templates.

About The Author: 

John Gordon, Director of Software Development, has been developing and testing AngularJS apps on multiple projects.

Facebook Twitter Vimeo Share to Stumble Upon More...

Comments

Great article John, thanks! Not sure what version of html2js you're using, but it should populate your $templateCache automatically. Look at the generated template JavaScript code for an example.

I think you're right, but the way the url's are set up with this app (a java backend), the request urls don't match the directory urls. If I didn't do anything with the template cache, the requested template wouldn't be resolved. I've fixed this by manually adding the path the angular code uses to make the request to the template cache. If you're directory structure matches the structure of your url's, you don't need to do this step.

You can configure `html2js` to strip/add prefixes when using `karma-ng-html2js-preprocessor` - no need to manualy map them.

Thanks for the tip.
In my case, templates was stored in several different directories, so I could include them with Karma matching (./**/) and I added a global "beforeEach" in my unit tests :

for ( var fileUrl in window.__html__ ) {
var template = window.__html__[fileUrl];
$templateCache.put(fileUrl,template);
}

Thank for this tip.
I am confused you mention line #s many times... But I am unable to see/open/download any associated files.

- Thomas

I'm using github's gists feature to display inline code. If you can't see it (what browser are you using, btw?), you can view all of them here: https://gist.github.com/gordonjl/5910896.

I don't see anything in either inlined code area--I tried current versions of both Firefox and Chrome--nothing there.

Silly question: do you have javascript enabled? If you wanted to go the 'extra mile', open up the debugger, load the page, and see if there are any errors. Sorry I can't be more helpful.

Hi John, this was really helpful. Thank you! I work at a big company in Westbrook that you may know :) and I've been trying to figure out how to do this testing for a few days, so I was quite surprised to find a local company with the answer then to recognize your name on top of that. We haven't met, but I actually replaced you. I no longer work in that role either. Small world! Thanks again! This was my last hurdle, now I'm happily testing away.

Glad it was able to help! Small world indeed. We should swap stories sometime. :-).

Hmmm...after I posted a comment, the code appeared, but not before.

Do you have experience chainging multiple directives to one module and being able to test that? Like a module full of validation directives?

Thanks John for a nice blog post!

I've noticed that Angular and Karma keep on changing so fast that many nice examples in the web get outdated pretty quickly :(.

To get this to work with AngularJS 1.1.5 and Karma 0.10.2, I had to use karma-ng-html2js-preprocessor instead of karma-html2js-preprocessor. So, replace "html2js" in the karma.conf.js with "ng-html2js" and remember to include the karma-ng-html2js-preprocessor in the plugins section.

Thanks for the post. Was struggling to get this working with grunt-angular-templates. Injecting the template into the $template cache made it work.

When I set this up the HTML in the element variable is never populated with the template. It remains:

Has anyone else had this happen to them?

you don´t need the templateCache....

Hi ,
I am getting this error while running test cases

WARN [preprocess]: Can not load "html2js", it is not registered!
Perhaps you are missing some plugin?

Any idea what can be the problem?

Add new comment

Filtered HTML

  • Web page addresses and e-mail addresses turn into links automatically.
  • Use [gist:####] where #### is your gist number to embed the gist.
  • Syntax highlight code surrounded by the <pre class="brush: lang">...</pre> tags, where lang is one of the following language brushes: as3, applescript, bash, csharp, coldfusion, cpp, css, delphi, diff, erlang, groovy, jscript, java, javafx, perl, php, plain, powershell, python, ruby, sass, scala, sql, vb, xml.
  • Allowed HTML tags: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
1 + 1 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
By submitting this form, you accept the Mollom privacy policy.