How to test HTMLImageElement.onload with jasmine + karma and Angular

Viewed 2605

I want to write tests for an Angular project that I'm working on but I'm an absolute beginner when it comes to testing (theory-wise), for instance, I have a service that I sincerely have no idea how I should test. This service is simply a container where information specific to the customer using the program (like the company logo, address, etc.) is stored for later retrieval by other components. However, it has a routine that loads the logo image file using HTMLImageElement and transforms it to a data URL using HTMLCanvasElement like such:

const image = new Image
image.onload = function(event) {
  const self = <HTMLImageElement> this
  const canvas = document.createElement('canvas')
  canvas.width = self.naturalWidth
  canvas.height = self.naturalHeight
  canvas.getContext('2d').drawImage(self, 0, 0)
  logoInstance._dataUrl = canvas.toDataURL('image/jpeg', 0.9)
}

For now, I just want to write a test that checks that the image was loaded and the data URL was stored successfully; something like:

it('...', () => {
  expect(logoInstance.dataUrl).toBeTruthy()
}

But the test sometimes fails (with Expected '' to be truthy) and sometimes pass. I think it is because the onload function is called asynchronously, and when the test fails it's because the image hasn't finished loading yet by the time the test is evaluated. However, even if I use async on the beforeEach function that kicks in the routine that I want to test, it still fails.

So my question is, how can I test a service that internally uses HTMLImageElement.onload to asynchronously load an image? Is there a way to evaluate the test only after onload has been called? I'm including below the complete code of what I have come up with after several attempts:

customer-info.ts

import { Injectable } from '@angular/core'

class Logo {

  private _dataUrl = ''

  get dataUrl(): string {
    return this._dataUrl
  }

  setImage(image: HTMLImageElement): void {
    const canvas = document.createElement('canvas')
    canvas.width = image.naturalWidth
    canvas.height = image.naturalHeight
    canvas.getContext('2d').drawImage(image, 0, 0)
    this._dataUrl = canvas.toDataURL('image/jpeg', 0.9)
  }
}


@Injectable()
export class CustomerInfoService {

  readonly logo = new Logo

  load(): void {
    const logoImage = new Image
    logoImage.onload = this.getOnImageLoadedCallback(this.logo)
    logoImage.src = './assets/images/customer-logo-lq.jpg'
  }

  private getOnImageLoadedCallback(logo: Logo): (event: Event) => void {
    return function(event) {
      const self = <HTMLImageElement> this
      logo.setImage(self)
    }
  }
}

customer-info.spec.ts

import { CustomerInfoService } from './customer-info'
import { async } from '@angular/core/testing'


describe('CoreModule.CustomerInfoService', () => {
  let service: CustomerInfoService = null

  beforeEach(async(() => {
    service = new CustomerInfoService
    service.load()
  }))

  it(
    'Should load the logo image and compute its data URL', 
    () => {
      expect(service.logo.dataUrl).toBeTruthy()
    }
  )
})

Thanks so much for your time and help.


Update

After reading a little bit more about jasmine and angular, I understand now that my attempt of using async with beforeEach was correct, but for some reason, the behavior of my test doesn't change -it still fails sometimes and passes others. So I tried to do the following:

1. fakeAsync and tick

I tried using fakeAsync instead of async which, as I understand, forces all asynchronous calls to be synchronous, but requires the use of tick to indicate that the image loading has finished. I refactored my test but got an exception that read Error: Cannot make XHRs from within a fake async test which makes sense since HTMLImageElement.src probably uses XHR asynchronous calls to load the image file. So I can't use this method with the current state of CustomerInfoService.

2. DoneFn and Spies

I found out that beforeEach, beforeAll and it take an instance of DoneFn as an argument which can be used to indicate that the evaluation has finished and it's time to evaluate the next test. So I came up with the idea that I could use spies to intercept calls to Logo.setImage and invoke DoneFn after it is finished. Apparently doing something like spyOn(...).and.callThrough().and.callFake() doesn't work. So I tried doing this:

describe('CoreModule.CustomerInfoService', () => {
  beforeEach((done: DoneFn) => {
    TestBed.configureTestingModule({
      imports: [
        HttpClientModule
      ],
      providers: [
        CustomerInfoService
      ]
    })
    service = TestBed.get(CustomerInfoService)

    const setImageBackup = service.logo.setImage
    spyOn(service.logo, 'setImage').and.callFake((image) => {
      setImageBackup(image)
      done()
    })

    service.load()
  })

  it('...', () => {
    expect(service.logo.setImage).toHaveBeenCalled()
    expect(service.logo.dataUrl).not.toBeNull()
  })
})

However, I get yet another exception; this time stating Cannot set property '_dataUrl' of undefined, which makes sense, since setImageBackup does not have anything defined for this since it's not a method of any instance, just a function. Still, I believe this is the approach that could work for my case, maybe if I try spying on HTMLImageElement.onload instead? However, even if this approach works for me, I feel like I'm doing something wrong here -that I'm still missing something. I hope this information let's you understand better what my problem is and what I'm trying to achieve.

0 Answers
Related