scope.$watch not called in directive when unit testing

angularjs, angularjs-directive, unit-testing

Solution

I just ran into a similar issue while testing a directive with isolate scope.

The reason it wasn't called is that `$digest` "processes all of the watchers of the current scope and its children" (http://docs.angularjs.org/api/ng.$rootScope.Scope). Now, you would think that a directive's bi-directionally bound variables (defined with "=") would be watching the test's scope, but I found that my `$watch` wasn't being called. Is this a bug in angular?

My solution was to wrap all the testing code after the `scope.$digest()` into a `$timeout`, e.g:

inject(function($timeout) {
  // change test scope variable
  scope.$digest();
  $timeout(function() {
    // assertions
    expect(...)
  }, 0);
}

Edit: Also see Unit testing an AngularJS directive that watches an an attribute with isolate scope

Problem

Problem I am testing a directive. The directive has a watch on a scope variable, but the watch is never called. Putting the `$watch` call directly inside the unit test works. Why isn't the `$watch` in the directive not called? Thanks! Details Here's my original test. The directive has a watch in it, looking at `'reEvalCreateDate'`. It's never called and the date string is always empty. Test with no watch statement in it ``` it('should inject a date string', function() { scope.reEvalCreateDate = reEvalCreateDateAsNumber; expect(elm.text()).toBe(''); scope.$digest(); expect(elm.text()).toBe(new Date(reEvalCreateDateAsNumber).toUTCString()); }); ``` As a test, I put the watch within the unit test. It is correctly called and the test passes. Source (modified to put the watch in the test itself) ``` it('should inject a date string', function() { scope.$watch('reEvalCreateDate', function(newValue, oldValue) { var date = new Date(newValue); scope.dateString = date.toUTCString(); dump(scope.dateString) }); scope.reEvalCreateDate = reEvalCreateDateAsNumber; expect(elm.text()).toBe(''); scope.$digest(); expect(elm.text()).toBe(new Date(reEvalCreateDateAsNumber).toUTCString()); //This passes! }); ``` Directive Source Code Here's the directive code with the watch statement in it. ``` link: function(scope, element, attrs) { scope.$watch('show',function(shouldShow){ console.log('should show reeval timer? ' + shouldShow); if(shouldShow) { $(element).fadeIn('slow'); } else if(shouldShow === false){ $(element).fadeOut('slow'); } }); //this statement's never called when testing!! scope.$watch('reEvalCreateDate', function(newValue, oldValue) { var date = new Date(newValue); scope.dateString = date.toUTCString(); }); } ``` Setup Code Here's all the init code for the test. ``` var elm, scope; var reEvalCreateDateAsNumber = 1359487598000; beforeEach(inject(function($rootScope, $compile) { // we might move this tpl into an html file as well... elm = angular.element( '<reeval-timer show="showTimePopup" re-eval-create-date="reEvalCreateDate" class="re-eval-timer" ng-cloak >' + '{{dateString}}' + '</reeval-timer>'); scope = $rootScope.$new(); $compile(elm)(scope); scope.$digest(); })); ``` Update Per the comment, I don't think that I am setting the scope on the directive; I don't see anything on the API that allows me to do that. I've tried several permutations of the `$compile` call, attempting to set the scope on it: create an element from html, pass it to compile. No luck. ``` elm = angular.element( '<div>' + '<tabs>' + '<pane title="First Tab">' + 'first content is {{first}}' + '</pane>' + '<pane title="Second Tab">' + 'second content is {{second}}' + '</pane>' + '</tabs>' + '</div>'); scope = $rootScope; $compile(elm)(scope); ``` and by creating an element first ``` var element = $compile('<p>{{total}}</p>')(scope); ``` If the issue is that I'm just not setting the scope on the directive, the error would likely be in my `beforeEach`, I think. Here it is: ``` describe('Reeval Timer', function() { var elm, scope,element; var reEvalCreateDateAsNumber = 1359487598000; beforeEach(inject(function($rootScope, $compile) { // we might move this tpl into an html file as well... elm = angular.element( '<reeval-timer show="showTimePopup" re-eval-create-date="reEvalCreateDate" class="re-eval-timer" ng-cloak >' + '</reeval-timer>'); scope = $rootScope.$new(); element = $compile(elm.contents())(scope); scope.$digest(); })); .... ```

Original source

Related problems