Kamilius Kamilius - 1 year ago 107
AngularJS Question

Unit Test Angularjs directive, which contains private timeout, with Jasmine

I have a directive, which should behave differently, depending on how much time passed since it's initialization:

am.directive('showText', () => ({
restrict: 'E',
replace: true,
scope: {
value: '@'
controller: ($scope, $timeout) => {
console.log('timeout triggered');

$scope.textVisible = false;

let visibilityCheckTimeout = $timeout(() => {
if (parseInt($scope.value, 10) < 100) {
$scope.textVisible = true;
}, 330);

// Clear timeout upon directive destruction
$scope.$on('$destroy', $timeout.cancel(visibilityCheckTimeout));

The problem is, that when I'm trying to test it with Jasmine, I can't seem to find a way to trigger this timeout in any way. Already tried a
(which actually throws an error, if I'll comment
call). But it's still not triggering that timeout's callback execution

describe('showText.', () => {
let $compile;
let $rootScope;
let $scope;
let $timeout;

const compileElement = (rootScope, value = 0) => {
$scope = rootScope.$new();
$scope.value = value;

const element = $compile(`


return element;

beforeEach(() => {

inject((_$compile_, _$rootScope_, _$timeout_) => {
$compile = _$compile_;
$rootScope = _$rootScope_;
$timeout = _$timeout_;

it(`Process lasts > 0.33s. Should show text.`, () => {
const VALUE = 30;
const element = compileElement($rootScope, VALUE);
const elementContent = element.find('.show-text__content');




expect(elementContent.text().trim()).toBe('Example text');

Test fails.

Can't find what am I doing wrong. Any tips on how to properly test such a case?


After some investigation, I've found that in this particular test-case, in
property isn't being evaluated by
service. And equals
. I've used same function already 10th of times, and can't get, why it's not taking a
's property as it was before.

Answer Source

The reason why this happens is that $timeout.cancel(visibilityCheckTimeout) launches unconditionally and immediately. Instead, it should be

$scope.$on('$destroy', () => $timeout.cancel(visibilityCheckTimeout));

There are things that can be done to improve testability (besides the fact that $timeout works here as one-time scope watcher and asks to be replaced with the one).

$timeout can be successfully spied:

beforeEach(module('app', ($provide) => {
  $provide.decorator('$timeout', ($delegate) => {
    var timeoutSpy = jasmine.createSpy().and.returnValue($delegate);
    angular.extend(timeoutSpy, $delegate);
    spyOn(timeoutSpy, 'cancel').and.callThrough();
    return timeoutSpy;

Private $timeout callback can be exposed to scope.

$scope._visibilityCheckHandler = () => {
  if (parseInt($scope.value, 10) < 100) {
    $scope.textVisible = true;
$timeout($scope._visibilityCheckHandler, 330);

This way all of the calls can be spied and get full coverage:

let directiveScope;

const element = $compile(`...`)($scope);

directiveScope = element.isolateScope();
spyOn(directiveScope, '_visibilityCheckHandler').and.callThrough();


expect($timeout).toHaveBeenCalledWith(directiveScope._visibilityCheckHandler, 330);

In this case here's no need to have separate specs for '>= 0.33s' and '< 0.33s' with flush delay argument, $timeout's inner work was already tested in Angular specs. Also, callback logic can be tested separately from $timeout spec.