1

I want to mock angular.element. And I want to ensure angular.element has been called a certain amount of times and that anguler.element.attr has been called too.

I have the following code:

var things = $scope.getThings();

for (var i = 0; i < things.length; i++) {
  if (things[i].type == "xyz") {
    angular.element("#thing-" + things[i].id)
      .attr("foo", things[i].bar);
  };
};

In my test I have:

var things = [
  {
    id: 1,
    type: "xyz",
    bar: 10
  },
  {
    id: 2,
    type: "abc",
    bar: 33
  }
];

spyOn($rootScope, "getThings").and.returnValue(things);
spyOn(angular, "element").and.returnValue();

$rootScope.doThings(); // call controller method

expect(angular.element.calls.count()).toBe(1);

But it gives the following error:

TypeError: undefined is not an object (evaluating 'angular.element("#thing-" + things[i].id).attr')

I also want my test to have something like:

expect(angular.element.attr.calls.count()).toBe(1);
expect(angular.element.attr).tohaveBeenCalledWith("foo", things[0].bar);
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
benjovanic
  • 537
  • 1
  • 5
  • 18

2 Answers2

1

try to add this code:

var spy;

beforeEach(function() {
    spy = spyOn(angular, 'element').....
});

afterEach(function() {
    spy.andCallThrough();
});

you can find more in: Jasmin docs

Oron Bendavid
  • 1,485
  • 3
  • 18
  • 34
1

The way chained methods should be spied or mocked solely depends on how they are defined on the spied objects.

In the case of Angular jqLite, or in your case, jQuery (both are transparently served through angular.element facade) chained methods are defined on constructor prototype, which is exposed on factory function as angular.element.prototype or jQuery.prototype (angular.element === jQuery when jQuery is loaded).

In order to spy on both angular.element and angular.element(...).attr it should be:

spyOn(angular, 'element').and.callThrough();
spyOn(angular.element.prototype, 'attr').and.callThrough();
...
expect(angular.element).toHaveBeenCalled();
expect(angular.element.prototype.attr).toHaveBeenCalled();

callThrough is important in this case because otherwise the whole chain should be stubbed manually.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • Am I right that using `callThrough` means I need the jQuery library included for the test? Using `callThrough` means the unit test will actually use jQuery and if jQuery breaks then my unit test breaks? – benjovanic Mar 29 '16 at 14:34
  • @benjovanic You haven't specified that you need to keep jQuery out of the spec in your question. But yes, you need to include it. And no, it shouldn't break, the code above won't throw even if there are no elements for '"#thing-...' selector (there should DOM fixtures for potentially absent elements when jQuery code is tested, but it is not a problem here). – Estus Flask Mar 29 '16 at 16:03