简体   繁体   中英

Unit Testing AngularJS Directives: scopes not updating?

Short version: I have a directive that is using a "new scope" (ie, scope:true ; not an isolate scope) which appears to work just fine when I'm interacting with the application, but which does not appear to update the scope in an observable way in the unit tests. Why are the changes to field on the directive's scope not observable in the unit test?

Example in Plunker: http://plnkr.co/edit/gpzHsX?p=preview

Here's the directive:

angular.module('app').directive('ngxBlurry', [
  function() {
    return {
      restrict:'C',
      replace:false,
      scope:true,
      link: function(scope, element, attrs) {
        element.bind('blur', function() {
          var val = element.val();
          scope.$apply(function(scope) {
            scope.field = val;
          });
        });

        scope.$watch('field', function(n, o) {
          if (n !== o) {
            scope.fields[n] = scope.fields[o];
            delete scope.fields[o];
          }
        });
      }
    };
  }
]);

It's getting used roughly as follows:

<div ng-controller="FooContoller">
  <div ng-repeat="(field, value) in fields">
    <input value="{{field}}" type="text" class="ngx-blurry"/>
    <strong>"field" is &laquo;{{field}}&raquo;</strong>
  </div>
</div>

Assuming that fields looks something approximately like:

$scope.fields = {
  'foo':true,
  'bar':true,
  'baz':true,
  '':true
};

And here's the unit test:

describe('ngxBlurry', function() {
  var scope,
      element,
      template = '<input value="{{field}}" type="text" class="ngx-blurry"/>';

  beforeEach(module('app'));
  beforeEach(inject(function($rootScope, $compile) {
    scope = $rootScope.$new();
    scope.fields = {'foo':true, '':true};
    scope.field = '';

    element = $compile(template)(scope);
  }));

  it('defers update until after blur', function() {
    spyOn(scope, '$apply');

    element.val('baz');
    element.triggerHandler('blur');

    // true:
    expect(scope.$apply).toHaveBeenCalled();
    // false!?
    expect(scope.field).toBe('baz');

    scope.$digest();
    // still false:
    expect(scope.field).toBe('baz');

    element.scope().$digest();
    // *still* false:
    expect(scope.field).toBe('baz');
    // also false:
    expect(element.scope().field).toBe('baz');
  });
});

Now, when interacting with the application:

  1. I can type into the input and any updates to the keys in $scope.fields are deferred until the blur event on that field.
  2. In the test, the Jasmine spy is reporting that the call to $apply is happening but ...
  3. ...the function that should be executed within the context of that $apply is (apparently) not getting called.
  4. ...nor does the $watch expression get called.

The directive itself in the context of the running application appears to operate just fine, but I cannot seem to find any reason why I cannot observe the changes with a unit test.

Barring a redesign of the directive (eg, to use isolate scope and $emit events): is there anything that I have missed here? Something I should change about the directive? or else some trick to make these changes observable in the unit test?

In a nutshell: don't forget .andCallThrough() on the spy.

Changing nothing about the directive itself, the working version of the test needed to be:

// everything about the `describe` stays the same...
it('defers update until after blur', function() {
  spyOn(scope, '$apply').andCallThrough();

  element.val('baz');
  element.triggerHandler('blur');

  expect(scope.$apply).toHaveBeenCalled();
  expect(element.scope().field).toBe('baz');
});

So...

  1. The Jasmine spy needed andCallThrough()
  2. Use element.scope() to access the correct scope.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM