5
votes

I need to test an AngularJs service which loads the image from url. Here is my service:

/*global angular, Image*/
(function () {
  'use strict';
  function SourceLoader($q, $log) {

    /**
     * Load an image from url and return a promise which is resolved when image is loading is done.
     * It return the images object as resolved promise.
     * @param url source of the image
     * @returns {Promise} unresolved promise of image.
     */
    function loadImage(url) {
      var deferred = $q.defer(),
          image = new Image();
      image.onload = function () {
        deferred.resolve(image);
      };
      image.onerror = function (error) {
        $log.error('Error while loading image from url ' + url);
        $log.debug(error);
        deferred.reject(error);
      };
      image.src = url;
      return deferred.promise;
    }

    /**
     * Load images from and array of urls nd return the array of loaded images. The order of returned
     * images is same as the order of passed urls.
     * @param urls sources if images in array.
     * @returns {Promise} unresolved promise.
     */
    function loadImages(urls) {
      var count = 0, deferred = $q.defer(),
          images = [],
          callback = function (image, index) {
            images.insert(index, image);
            count += 1;
            if (count === urls.length) {
              //All images are loaded. High time to resolve promise.
              deferred.resolve(images);
            }
          };
      urls.forEach(function (url, index) {
        var image = new Image();
        image.onload = function () {
          callback(image, index);
        };
        image.onerror = function (event) {
          $log.error('Error while loading image from url ' + url);
          $log.debug(event);
          callback(event, index);
        };
        image.src = url;
      });
      return deferred.promise;
    }

    return {
      loadImage: loadImage,
      loadImages: loadImages
    };
  }

  var app = angular.module('myCanvasModule'),
      requires = [
        '$q',
        '$log',
        SourceLoader
      ];
  app.factory('SourceLoaderService', requires);
}());

I am using Karma with Jasmine to for test suits. My test suits is something like this:

/*global describe, TestUtils, beforeEach, it, module, inject, SourceLoaderServiceTestData, done, expect*/
(function () {
  'use strict';
  describe('myCanvasModule: Services: SourceLoaderService-->\n\t', function () {
    var SourceLoaderService, getTestData;

    getTestData = TestUtils.getTestData.bind(null, SourceLoaderServiceTestData);

    beforeEach(function () {
      module('myCanvasModule');
    });

    beforeEach(function () {
      inject(function ($injector) {
        SourceLoaderService = $injector.get('SourceLoaderService');
      });
    });

    describe('SourceLoaderService.loadImage-->\n\t', function () {
      var testData;
      beforeEach(function () {
        testData = getTestData('loadImage');
      });

      it('should load the source and return the image as response', function (done) {
        var image;
        SourceLoaderService.loadImage(testData.validSource).then(function (response) {
          image = response;
          done();
          console.log('called');
        },function (error) {
          console.log('called', error);
          done();
        });
      });

    });

  });
}());

When I run the test suits I get:

Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.

I don't know what is going wrong. I am new to Karma and Jasmine. Thanks for the help.

5
Maybe use Promise.all as return statement in loadImages. Then you can skip your count book keeping.lipp
sorry did not get you.Hitesh Kumar
is your image source url valid?lipp
yes. Even if I put any other url like static-s.aa-cdn.net/img/gp/20600002150126/… then is also shows up the same errorHitesh Kumar

5 Answers

2
votes

To test any usage of the $q provider in AngularJS you'll have to cause a digest cycle to allow any Promises to be resolved under-the-hood. You should try the following:

it('should load the source and return the image as response', function (done) {
  var image;

  SourceLoaderService.loadImage(testData.validSource)
    .then(function (response) {
      // ... success check
    },function (error) {
      // ... error check
    });

  // Call apply for the above Promise to resolve
  $scope.$apply();
}

Edit:

In response to the comment, you'll also need to create the $scope variable as follows:

var SourceLoaderService, $scope;

beforeEach(function () {
  inject(function ($injector) {
    SourceLoaderService = $injector.get('SourceLoaderService');
    $scope = $injector.get('$rootScope').$new();
  });
});
2
votes

I was able to reproduce this and also able to run your test case successfully too. I am not sure why do you need done thingie there but below changes worked for me.

it('should load the source and return the image as response', function () {
  var image;
  sourceLoader.loadImage('https://s0.2mdn.net/viewad/5406241/6-jira-software_therapy-agile-tools_free-trial_static_728x90.png').then(function (response) {
    image = response;
    //setTimeout(done(), 60000);
    console.log('called');
  },function (error) {
    console.log('called', error);
    //done();
  });
});
2
votes

Having an argument in your "it" function will cause the test to perform an async call.

it('should load the source and return the image as response', function () {
  var image;
  sourceLoader.loadImage(testData.validSource).then(function (response) {
    image = response;
    console.log('called');
  },function (error) {
    console.log('called', error);
  });
});

In order to use the "done" callback to test async calls, you should move some code to the "beforeEach" function. Here's a link for reference.

1
votes

It's likely that image load takes more time than default timeout interval.
Try something like this:
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000. It sets time to wait for done() call to 30 sec.

1
votes

Array.insert is just not a function. You can extend Array to have an insert function of course. But there is a much simpler way. Either just call images[:N] = image or just use Promise.all:

This will simplify your loadImages function a lot:

var loadImages = function() {
  var loadImagePromises = urls.map(function(url) {
    return loadImage(url)
  })
  return Promise.all(loadImagePromises)
}