How to mock Image object with Karma (Angular 7)?

Viewed 1658

I'm trying to mock the default DOM Image object for unit testing an Angular service.

The service is simple, it checks the "webP" format support :

import {Injectable} from '@angular/core';
import {Promise} from 'es6-promise';
import {environment} from '../../../../environments/environment';

@Injectable({
  providedIn: 'root'
})
export class AppLoadService {

  constructor() {
  }

  initializeApp(): Promise<any> {
    return new Promise((resolve) => {
      console.log(`initializeApp:: inside promise`);
      if (typeof Image === 'undefined') {
        console.log(`initializeApp:: Image undefined`);
        resolve();
        return;
      }
      const webP = new Image();
      webP.onload = () => {
        console.log(`initializeApp:: WebP support: true`);
        environment.webP = true;
        resolve();
      };
      webP.onerror = () => {
        console.log(`initializeApp:: WebP support: false`);
        resolve();
      };
      webP.src = 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA';
    });
  }
}

I found a way to check the webP support (default in chromium where Karma is running), and a way to check fallback on Image undefined.

But I cannot find a way to checks the onerror fallback...

Here is my spec file :

import {TestBed} from '@angular/core/testing';
import {AppLoadService} from './app-load.service';
import {environment} from '../../../../environments/environment';

describe('AppLoadService', () => {
  let service: AppLoadService;
  const originalImage = Image;

  beforeEach(() => TestBed.configureTestingModule({}));

  beforeEach(() => {
    service = TestBed.get(AppLoadService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  it('should resolve with webP', (done) => {
    const test = () => {
      Image = originalImage;
      service.initializeApp().then(() => {
        expect(environment.webP).toBe(true);
        done();
      });
    };
    test();
  });

  it('should resolve without webP (A)', (done) => {
    const test = () => {
      Image = undefined;
      service.initializeApp().then(() => {
        expect(environment.webP).toBe(false);
        done();
      });
    };
    test();
  });

  it('should resolve without webP (B)', (done) => {
    // How to force Image to throw "onerror" ?
    const test = () => {
      Image = originalImage;
      service.initializeApp().then(() => {
        expect(environment.webP).toBe(false);
        done();
      });
    };
    test();
  });
});

The question is on should resolve without webP (B) test, at the end of the file..

Moreover, is there a better way to check undefined Image object or onload callback ?

Thanks !


EDIT

Can't get it works as it is, so I change the service constructor to provide "Image" dependency.

constructor(@Inject('Image') image: typeof Image) {
  this.image = image;
}

Have to load the module like that :

providers: [
  AppLoadService,
  // [...]
  {provide: 'Image', useValue: Image},
]

And each resolve() now includes environment.webP result. Otherwise, individual test are a real pain, environment is randomly rewritten before being tested.

With a simple Mock it works like this :

import {TestBed} from '@angular/core/testing';
import {AppLoadService} from './app-load.service';

class MockImageFail {
  public onerror: Function;
  private _src: string;

  set src(src) {
    this._src = src;
    if (this.onerror) {
      this.onerror();
    }
  }
}

describe('AppLoadService', () => {

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [{provide: 'Image', useValue: Image}]
    });
  });

  it('should be created', () => {
    const service = TestBed.get(AppLoadService);
    expect(service).toBeTruthy();
  });

  it('should resolve with webP', (done) => {
    const service = TestBed.get(AppLoadService);
    service.initializeApp().then((supportWebP) => {
      expect(supportWebP).toBe(true);
      done();
    });
  });

  it('should not resolve without webP (A)', (done) => {
    TestBed.overrideProvider('Image', {useValue: undefined});
    const service = TestBed.get(AppLoadService);
    service.initializeApp().then((supportWebP) => {
      expect(supportWebP).toBe(false);
      done();
    });
  });

  it('should not resolve without webP (B)', (done) => {
    TestBed.overrideProvider('Image', {useValue: MockImageFail});
    const service = TestBed.get(AppLoadService);
    service.initializeApp().then((supportWebP) => {
      expect(supportWebP).toBe(false);
      done();
    });
  });
});

I'm not really happy with that and I'm sure there is another better way :/

1 Answers

I had a similar need, but I didn't want to create a provider just for Image as I only had the need for it in one place. But I still needed to mock Image to be able to write my specs.

I do have an existing provider for window so I decided to use that instead. I am writing a web app, so my 'global' is actually window. This is according to MDN: "In a web browser, any code which the script doesn't specifically start up as a background task has a Window as its global object. This is the vast majority of JavaScript code on the Web." I changed new Image() to new this.window.Image() and I inject window in my constructor like so:

constructor(@Inject(WINDOW) private readonly window: any) {}

My check then looked like this:

public canLoadImage(uri: string): Promise<boolean> {
  const image = new this.window.Image();
  this.promise = new Promise((resolve) => {
    image.onload = () => resolve(true);
    image.onerror = () => resolve(false);
  });
  image.src = `${uri}?${Math.random()}`;
  return this.promise;
}

So I was able to test this by mocking and injecting window like so:

beforeEach(() => {
  mockImage = {};
  mockWindow = { Image: () => mockImage };
  service = new MyService(mockWindow);
});
 
it('resolves false when youtube is not reachable', fakeAsync(() => {
  const successSpy = jasmine.createSpyObject('success');
  service.canLoadImage('https://example.com').then(successSpy);
  mockImage.onerror();
  flushMicrotasks();

  expect(successSpy).toHaveBeenCalledWith(false);
}));

it('resolves true when youtube is reachable', fakeAsync(() => {
  const successSpy = jasmine.createSpyObject('success');
  service.canLoadImage('https://example.com').then(successSpy);
  mockImage.onload();
  flushMicrotasks();

  expect(successSpy).toHaveBeenCalledWith(true);
}));

Note that you could easily change the code to reject when their is a failure and test that by calling mockImage.onerror() directly.

Note also that you have to create the promise in the same zone within which you call flushMicrotaskt (or tick). Thus why I set up the success spy, call the canLoadImage, and call the mocked image methods in the same fakeAsync method call.

One reason I did this in this way is to avoid an actual external call to fetch the image. Because my Image instance was just an object on which to accumulate effects, I didn't have to worry about it actually loading the image.

Related