0

I have the following directive which tells me whether or not the image i'm trying to use has loaded successfully or not:

return {
    restrict: 'A',
    scope: {
        imageLoad: '@'
    },
    link: function(scope, element, attrs) {

        attrs.$observe('imageLoad', function (url) {
            var deferred = $q.defer(),
                image = new Image();

            image.onerror = function () {
                deferred.resolve(false);
            };

            image.onload = function () {
                deferred.resolve(true);
            };

            image.src = url;
            return deferred.promise;
        });

    }
};

All i then want to do is two simple tests that test image.onerror and image.onload but i only seem to get into the on error function, here's what i have so far:

compileDirective = function() {
    var element = angular.element('<div data-image-load="http://placehold.it/350x150"></div>');
    $compile(element)(scope);
    $rootScope.$digest();
    return element;
};

beforeEach(inject(function (_$compile_, _$rootScope_) {
    $compile = _$compile_;
    $rootScope = _$rootScope_;
    scope = $rootScope.$new();
}));

it('should do something', function() {
    var compiledElement, isolatedScope;
    compiledElement = compileDirective();
    isolatedScope = compiledElement.isolateScope();
    expect(true).toBe(true);
});

obviously this test passes as it just expects true to be true, however in terms of coverage this gets into the onerror function, so i somehow need to test that the deferred.promise should return false.

so ultimately a two part question, how do i get the result of the deferred.resolve?

and secondly how do i get into the onload function?

i've had a look around and seen some suggestions of adding the following:

element[0].setAttribute('imageLoad','http://placehold.it/350x150');
$compile(element)(scope);
element.trigger('imageLoad');

and leaving the data-image-load="" blank, but haven't seemed to have any luck, any suggestions would be great.

gardni
  • 1,384
  • 2
  • 24
  • 51
  • This is not related to your question, but is there any special reason you are using `attrs.$observe`? Right now you could just remove that and replace `url` with `scope.imageLoad`. Right now the anonymous function passed to `$observe` just returns the promise, but no one will be able to look at the result. What is the point of the directive? How is it supposed to be used? – tasseKATT Jan 05 '17 at 21:02
  • @tasseKATT basically the image path in the real application will be dynamic and as a result can change, so it needs to be re-triggered. the promise makes sure it's loaded, if it failed i'd serve a fallback image, i've omitted some code from the above example, in where i set a fallback image using element.css (background image) – gardni Jan 06 '17 at 10:18
  • So the directive itself is the only one reacting to the result of the promise? – tasseKATT Jan 06 '17 at 10:20
  • Can you show how the result of the promise is used to set the fallback? – tasseKATT Jan 06 '17 at 10:35
  • `image.onerror = function () { deferred.resolve(false); element.css({ 'background-image': 'url(' + fallback + ')' }); }; ` – gardni Jan 06 '17 at 10:42
  • i've basically stolen one of the answers from Mehul here http://stackoverflow.com/questions/16310298/if-a-ngsrc-path-resolves-to-a-404-is-there-a-way-to-fallback-to-a-default/17122325#17122325 - but used it to set a background image if the image exists – gardni Jan 06 '17 at 10:46
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/132478/discussion-between-tassekatt-and-gardni). – tasseKATT Jan 06 '17 at 10:52

1 Answers1

2

From what you have shown, you shouldn't need the promise at all.

Even if you did, since the promise is only used internally and no one is using the result, it should be considered an implementation detail and your test shouldn't care about it.

Let us say you have the following directive:

app.directive('imageLoad', function() {
  return {
    restrict: 'A',
    scope: {
      imageLoad: '@'
    },
    link: function(scope, element, attrs) {

      var fallback = 'http://placekitten.com/g/200/300';

      attrs.$observe('imageLoad', function(url) {

        var image = new Image();

        image.onerror = function(e) {

          setBackground(fallback);
        };

        image.onload = function() {

          setBackground(url);
        };

        image.src = url;
      });

      function setBackground(url) {
        element.css({
          'background': 'url(' + url + ') repeat 0 0'
        });
      }
    }
  };
});

Demo of it in use: http://plnkr.co/edit/3B8t0ivDbqOWU2YxgrlB?p=preview

From an outside perspective, the purpose of the directive is to set the element's background to either the passed url or to the fallback, based on if the passed url is working.

So what you want to test is:

  1. The passed url is working - should use passed url as background.
  2. The passed url is not working - should use fallback as background.

This means that you need to be able to control if the image can be loaded or not.

To prevent any network traffic in your test I would recommend using data URIs instead of URLs.

Example:

var validImage = 'data:image/jpeg;base64, + (Valid data omitted)';
var invalidImage = 'data:image/jpeg;base64,';

Full example:

describe('myApp', function() {

  var $compile,
    $rootScope;

  beforeEach(module('myApp'));

  beforeEach(inject(function(_$compile_, _$rootScope_) {
    $compile = _$compile_;
    $rootScope = _$rootScope_;
  }));

  var validImage = 'data:image/jpeg;base64, + (Valid data omitted)';
  var invalidImage = 'data:image/jpeg;base64,';

  compileDirective = function(url) {
    var element = angular.element('<div data-image-load="' + url + '"></div>');
    return $compile(element)($rootScope.$new());
  };

  it('should use correct background when image exists', function(done) {

    var element = compileDirective(validImage);

    $rootScope.$digest();

    setTimeout(function() {

      var background = element.css('background');
      expect(background).toBe('url("' + validImage + '") 0px 0px repeat');

      done();

    }, 100);
  });

  it('should use fallback background when image does not exist', function(done) {

    var element = compileDirective(invalidImage);

    $rootScope.$digest();

    setTimeout(function() {

      var background = element.css('background');
      expect(background).toBe('url("http://placekitten.com/g/200/300") 0px 0px repeat');

      done();

    }, 100);
  });
});

Note that since loading of an image is asynchronous you need to add a bit of waiting in your tests.

You can read more about how it is done with Jasmine here.

Demo: http://plnkr.co/edit/W2bvHih2PbkHFhhDNjrG?p=preview

tasseKATT
  • 38,470
  • 8
  • 84
  • 65
  • 1
    thank you for such a great answer, clearly spent a lot of time understanding the problem and working out a solution, i was worried my approach originally might have been incorrect, this solution worked perfectly, i particularly liked your suggestion for the base64 encoding. – gardni Jan 06 '17 at 20:25