0

First iteration of this question: Unit testing Angular directive with $http

I created a directive that, when bound to an input, responds to blur/keyup events by calling a service before modifying the DOM. This service relies on $http and is therefore asynchronous. I've written a functional unit test for the service, but for the life of me I can't write a working test for the directive.

describe('SignupForm directives', function () { 
    var form,       // el to which directive is applied
    scope,
    userService,
    el;

beforeEach(function() {
    module('signupform.directives');
});

beforeEach(function() {
    inject(function ($injector, $rootScope, $compile, $q, $timeout) {
        scope = $rootScope.$new();
        el = $compile('<input id="username" type="text" name="username" ng-model="user.username" sg-valid-username />')(scope);

        userService = $injector.get('userService');
        scope.user = { username: 'test_username' };
        spyOn(userService, 'checkIfUserExists');
    });
});

   it('Directive fires on blur', function() {
    scope.$digest();

    // build blur event and fire it
    var blurEvent = $.Event('blur');
    angular.element(el).triggerHandler(blurEvent);

    expect(userService.checkIfUserExists).toHaveBeenCalled();
    });
});

I have proven using the debugger that the service—userService.checkIfUserExists—gets called. It's just the expect() happens before then and fails the test.

I have no clue how to write this asynchronously, though. Jasmine 2.0's done() concept is mind-boggling, and userService does not otherwise need a callback to pass in.

How can I approach this?

2
  • Add scope.$digest() after angular.element(el).triggerHandler(blurEvent); Commented Oct 24, 2014 at 1:19
  • @merlin: I moved scope.$digest() after that line and before the expect(), then reran karma. Error is still the same: Expected spy checkIfUserExists to have been called. Commented Oct 24, 2014 at 2:31

1 Answer 1

1

Use $timeout instead of setTimeout in your driective, so that you can manually flush the pending timeout in unit tests.

.directive('sgValidUsername', ['$timeout', 'userService', function ($timeout, userService) {
    return {
        restrict: 'A',

        link: function(scope, element) {
            var promise;
            element.on('blur keyup', function(e) {
              $timeout.cancel(promise);
              promise = $timeout(function() {
                userService.checkIfUserExists();
              }, 1000);
            });
        }
    }
}])

In your unit test, call $timeout.flush() after el.triggerHandler('blur') to manually trigger the timeout callback(which calls your checkIfUserExists).

it('Directive fires on blur', function() {
  el.triggerHandler('blur');
  $timeout.flush();//Flush pending timeouts
  expect(userService.checkIfUserExists).toHaveBeenCalled();
});

See this plnkr for a full working example.

Sign up to request clarification or add additional context in comments.

1 Comment

Brilliant! I had never even heard of $timeout. Thanks.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.