1

I am testing a directive with isolate scope (angular) and Jasmine test framework. Everything is working fine with one oddity: if I change the variable type on $scope from string to number, Jasmine continues to see it as a string even though the directive code itself properly treats it as a number.

I understand why this might make intuitive sense - the parm is interpolated initially as a string and maybe Angular has some mechanism for ensuring it stays a string. If true, I don't get how and where it is implemented in the Angular source (or even how it could be).

In sum: Why does Jasmine see a string and the directive see a number?

Here's my code:

Markup

<pane id={{myId}}></pane>

Directive

.directive('testPane', function () {

    function thisController($scope) {

       //$scope.id is string '1'
       $scope.id = parseInt($scope.id);

       //now it is number 1 (as expected)
       console.log($scope.id);


    };


    return {
        restrict: 'E',
        transclude: true,
        scope: { id: '@'},
        controller: thisController,
        link: function (scope, element, attrs, tabsController) {


        },
        templateUrl: 'test/test.html',
        replace: true
    };
})

Jasmine Spec

it("changes id from string to number", function () {

    var element,
        elementScope;

    $rootScope.myId = 1;

    element = $compile(html)($rootScope);

    $timeout(function () {
        businessTabScope = element.isolateScope();
        $rootScope.$digest();


        expect(elementScope.id).toBeDefined();

        //id is text '1' instead of number 1. Why?
        expect(elementScope.id).toEqual($rootScope.myId);
    }, 0, false);



   $timeout.flush();


});
5
  • 2
    This is not an angular issue. When you read data from the dom it comes as string and Angular cannot make assumptions about the type as javascript being a dinamic language Commented May 26, 2015 at 17:09
  • 1
    Not following. I am reading a property on (isolate) scope. Inside the directive the type is a number. From Jasmine it is a string. Both are looking at the same object - scope. I don't see how the DOM comes into play. Commented May 26, 2015 at 17:15
  • Perhaps your controller code (that parses the number to an int) has not yet been executed when you're checking the value in the test? I suspect that's why you're using $timeout here... Have you tried adding log statements to see which code is executing? Have you tried increasing the timeout value? By the way, I've never had to use $timeout when testing my directives ... I just call $digest() on the directive's parent scope. Commented May 26, 2015 at 17:19
  • the $timeout is there because the directive uses templateUrl. If that is not added, the call to retreive isolateScope will fail b/c $http hasn't resolved in time. Commented May 26, 2015 at 17:37
  • @Sunil I've verified that the controller code executes first. This can be done by introducing another $scope var defined only within the controller. The Jasmine code correctly reads it. Commented May 26, 2015 at 17:40

1 Answer 1

1

Problem

When you define an @ binding, Angular creates an observer for that attribute so it can update the local property's value if that attribute later changes. You can find that particular code at line 7676 (Angular 1.3.15):

case '@':
  attrs.$observe(attrName, function(value) {
    isolateBindingContext[scopeName] = value;
  });  
  ...

Now let's see what the docs on $observe says about it (emphasis mine):

Observes an interpolated attribute.

The observer function will be invoked once during the next $digest following compilation. The observer is then invoked whenever the interpolated value changes.

So that observer is guaranteed to be invoked once after the directive is compiled, that is, after both controller and link functions are executed. And since you are assigning a new value to the id property in the controller function, it gets overridden back to its original value - which is a string since it's a DOM attribute.

Solution

Given the code you have posted, it seems that you only care about the id first value. To grab it you can simply do the following:

.directive('testPane', function () {
  return {
    ...
    scope: {},
    controller: function($scope, $attrs) {
      $scope.id = parseInt($attrs.id, '10');
    }
  };
});

You can keep using an @ binding if you want, but make sure the converted value is stored in another property of the scope so it doesn't get overridden - either after the directive is compiled or later on.

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

Comments

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.