Search code examples
angularjsunit-testingkarma-jasmineangular-mockangular-decorator

How do I test a decorator which is a wrapper around $timeout?


I have the following decorator which wraps around the original $timeout from within $rootScope. When used inside controllers, it will help by canceling the $timeout promise when the scope is destroyed.

angular.module('MyApp').config(['$provide', function ($provide) {

    $provide.decorator('$rootScope', ['$delegate', function ($delegate) {

        Object.defineProperty($delegate.constructor.prototype, 'timeout', {
            value: function (fn, number, invokeApply) {
                var $timeout = angular.injector(['ng']).get('$timeout'),
                    promise;

                promise = $timeout(fn, number, invokeApply);

                this.$on('$destroy', function () {
                    $timeout.cancel(promise);
                });
            },
            enumerable: false
        });

        return $delegate;
    }]);

}]);

But how do I properly unit test this? I kinda see 2 tests I should be doing here... 1) Check if the original $timeout was called when $rootScope.timeout() is called and 2) check if the promise is cancelled when the scope is destroyed.

Here's my current test suite for this:

describe('MyApp', function () {
    var $rootScope;

    beforeEach(function () {
        module('MyApp');

        inject(['$rootScope', function (_$rootScope_) {
            $rootScope = _$rootScope_;
        }]);
    });

    describe('$timeout', function () {
        it('<something>', function () {
            $rootScope.timeout(function () {}, 2500);

            // Test if the real $timeout was called with above parameters

            $rootScope.$destroy();

            // Test if the $timeout promise was destroyed
        });
    });

});

The only thing that this does is giving me 100% coverage. But that's not what I want... How do I properly test this?


Solution

  • Since nobody was able to help me and I really wanted to have this done, I eventually found my own solution. I'm not sure it is the best solution but I think it does the trick.

    Here's how I solved it:

    describe('MyApp', function () {
        var $rootScope,
            $timeout,
            deferred;
    
        beforeEach(function () {
            module('MyApp');
    
            inject(['$rootScope', '$q', function (_$rootScope_, _$q_) {
                $rootScope = _$rootScope_;
                deferred = _$q_.defer();
            }]);
    
            $timeout = jasmine.createSpy('$timeout', {
                cancel: jasmine.createSpy('$timeout.cancel')
            }).and.returnValue(deferred.promise);
    
            spyOn(angular, 'injector').and.returnValue({
                get: function () {
                    return $timeout;
                }
            });
        });
    
        describe('$timeout', function () {
            it('should set the timeout with the specified arguments', function () {
                $rootScope.timeout(angular.noop, 250, false);
                expect($timeout).toHaveBeenCalledWith(angular.noop, 250, false);
            });
    
            it('should cancel the timeout on scope destroy event', function () {
                $rootScope.timeout(angular.noop, 250, false);
                $rootScope.$destroy();
    
                expect($timeout.cancel).toHaveBeenCalledWith(deferred.promise);
            });
        });
    
    });