Friday, August 22, 2014

QUnit tests on AngularJS directives

Recently, I had an little homework assignment to create a "signup" web application using Angular.js. You can see the app here.

As part of that assignment, I created an Angular directive to validate the data in the "password" and "verification" text inputs.  Here's what the Angular directive looked like:

// match password and verification
app.directive('match', [function () {
  return {
    require: 'ngModel',
    link: function(scope, elem, attrs, ctrl) {
      scope.$watch('['+attrs.ngModel+', '+attrs.match+']',
        ctrl.$setValidity('match', value[0] === value[1]);
      }, true);

But I wanted to use QUnit to test this directive.  I spent a ton of time, trying a zillion things, which didn't work because I was a newbie to both Angular and QUnit (and using them together).  But, finally, I found the correct combination.

// create test bed
var injector = angular.injector(['ng', 'ngMock', 'signupApp']);

var init = {
  setup: function() {
    this.$scope = injector.get('$rootScope').$new();

module('tests', init);

// test the 'match' directive
QUnit.test('match', function() {
  var html = '<form id="myform" name="signupController" ng-controller="signupController"><input id="password" ng-model="password" match="verification"></input><input id="verification" ng-model="verification" match="password"></input></form>';
  var $compile = injector.get('$compile');
  var element = $compile(html)(this.$scope);
  this.$scope.password = 'passw0rd';
  this.$scope.verification = 'passw0rd';
  ok(element.scope().signupController.$valid, '$valid is false');
  this.$scope.password = 'passw0rd';
  this.$scope.verification = 'passw0rd2';
  ok(!element.scope().signupController.$valid, '$valid is true when it should be false');

My first mistake was that it took me a long time to figure out that I needed angular-mocks.js to create a fully functional test bed.  The ngMock module is needed to add lots of support to the Angular test harness.  While I could get a simple test harness running without angular-mocks.js, testing that required the controller (and probably ngModel) required angular-mocks.js.

My second mistake was that it took me a long time to realize that the Angular scope and the Angular controller are different.  (Well, duh.)  Finally, I discovered that, if I added a "name" attribute to the controller element, a property giving access to the controller would be available in the scope.

This may be obvious to Angular experts but I spent a ton of time traveling around on Google and I never found any posts that were on point.