Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to test an AngularJS watch using debounce function with Jasmine

I have a controller with a watch that uses debounce from lodash to delay filtering a list by 500ms.

$scope.$watch('filter.keywords', _.debounce(function () {
  $scope.$apply(function () {
    $scope.filtered = _.where(list, filter);
  });
}, 500));

I am trying to write a Jasmine test that simulates entering filter keywords that are not found followed by keywords that are found.

My initial attempt was to use $digest after assigning a new value to keywords, which I assume didn't work because of the debounce.

it('should filter list by reference', function () {
  expect(scope.filtered).toContain(item);
  scope.filter.keywords = 'rubbish';
  scope.$digest();
  expect(scope.filtered).not.toContain(item);
  scope.filter.keywords = 'test';
  scope.$digest();
  expect(scope.filtered).toContain(item);
});

So I tried using $timeout, but that doesn't work either.

it('should filter list by reference', function () {
  expect(scope.filtered).toContain(item);
  $timeout(function() {
    scope.filter.keywords = 'rubbish';
  });
  $timeout.flush();
  expect(scope.filtered).not.toContain(item);
  $timeout(function() {
    scope.filter.keywords = 'test';
  });
  $timeout.flush();
  expect(scope.filtered).toContain(item);
});

I have also tried giving $timeout a value greater than the 500ms set on debounce.

How have others solved this problem?

EDIT: I've found a solution which was to wrap the expectation in a $timeout function then call $apply on the scope.

it('should filter list by reference', function () {
  expect(scope.filtered).toContain(item);
  scope.filter.keywords = 'rubbish';
  $timeout(function() {
    expect(scope.filtered).not.toContain(item);
  });
  scope.$apply();
  scope.filter.keywords = 'test';
  $timeout(function() {
    expect(scope.filtered).toContain(item);
  });
  scope.$apply();
});

I'm still interested to know whether this approach is best though.

like image 870
gwhn Avatar asked Jun 11 '14 16:06

gwhn


2 Answers

This is a bad approach. You should use an angular-specific debounce such as this that uses $timeout instead of setTimeout. That way, you can do

  $timeout.flush();
  expect(scope.filtered).toContain(item);

and the spec will pass as expected.

like image 65
FlavorScape Avatar answered Oct 19 '22 07:10

FlavorScape


I've used this:

beforeEach(function() {
    ...
    spyOn(_, 'debounce').and.callFake(function (fn) {
        return function () {
            //stack the function (fn) code out of the current thread execution
            //this would prevent $apply to be invoked inside the $digest
            $timeout(fn);
        };            
    });
});

function digest() {
    //let the $watch be invoked
    scope.$digest();
    //now run the debounced function
    $timeout.flush();
}

it('the test', function() {
    scope.filter.keywords = ...;
    digest();
    expect(...);
});

Hope it helps

like image 21
Alex Pollan Avatar answered Oct 19 '22 08:10

Alex Pollan