Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

scope.$watch not called in directive when unit testing

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();
    }));

....
like image 986
John Gordon Avatar asked Jan 30 '13 20:01

John Gordon


2 Answers

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

like image 75
alalonde Avatar answered Nov 15 '22 23:11

alalonde


I was experiencing what seems like the same problem. In my case, my directive had a templateUrl set rather than a hardcoded template, so in my unit test I also had to set up a mocked http request by calling:

$httpBackend.expectGET('/template.html').respond('<div></div>');

Everything else looked the same as your example, and my watch wasn't being called.

Then I remembered when using $httpBackend for mocking the request, you have to call $httpBackend.flush() like so:

$compile(elem)(scope);
$httpBackend.flush();

As soon as the flush call was hit, my watch was called.

Hope this helps....

Jason

like image 32
Jason Capriotti Avatar answered Nov 15 '22 23:11

Jason Capriotti