2

I haven't found any way of doing an unit-test with Jasmine/Karma on an Angular2component which renders a chart from Google Charts. So I propose my own Q&A in the hope that it could help other people also testing it:

What could be a good way to write a reliable test for a component like the one on the example below, that has a function to update the chart view when window is resized?

import { Component, OnInit, Input } from '@angular/core';
import { Router } from '@angular/router';

declare var google: any;

@Component({
  selector: 'app-chart',
  templateUrl: './chart.component.html',
  styleUrls: ['./chart.component.scss'],
})
export class ChartComponent implements OnInit {
  @Input()
  chartData: string[][];

  options: {} = {
    height: 320,
  };

  constructor(private router: Router) {}

  ngOnInit() {
    google.charts.load('current', { packages: ['corechart'] });
    google.charts.setOnLoadCallback(() => this.drawChart(this.chartData));
    window.addEventListener('resize', () => {
      if (this.router.url === '/chart') {
        return this.drawChart(this.chartData);
      }
    });
  }

  drawChart(chartData) {
    const data = google.visualization.arrayToDataTable(chartData);
    const chart = new google.visualization.LineChart(
      document.querySelector('#chart'),
    );
    chart.draw(data, this.options);
  }
}
Анна
  • 1,248
  • 15
  • 26
  • I did not downvote so I can't comment why someone else did - but I doubt it was because you answered your own question. It is possible it was done because there didn't appear to be any effort made to [solve your own problem first](https://idownvotedbecau.se/noattempt/), although you clearly showed in your answer below that you have put a lot of effort into solving it! Next time you may want to include all that in the original question. Acually, I think this was an excellent question and good attempted solution on your part and upvoted your Question to more than cancel out the downvote. :) – dmcgrandle Dec 14 '18 at 21:38
  • Thank you for the clarification @dmcgrandle. I've added an explanation in the question stating this is a self Q&A to avoid misunderstanding. :) – Анна Dec 15 '18 at 00:14

2 Answers2

1

The following snippet would correctly test all functionalities, checking rendering methods of Google Charts on Angular and also the functionality to update the chart when the window is resized:

import {
  TestBed,
  ComponentFixture,
  async,
  fakeAsync,
  tick,
} from '@angular/core/testing';
import { ChartComponent } from './chart2';
import { GoogleChartsModule } from 'angular-google-charts';
import { Observable } from 'rxjs';
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { Location } from '@angular/common';
import { RouterTestingModule } from '@angular/router/testing';

@Component({
  template: '',
})
class DummyComponent {}

const mockChartData = [
  ['Date', 'Variable X', 'Variable Y'],
  ['29/sep.', '30', '29'],
  ['30/sep.', '30', '29'],
  ['01/oct.', '30', '29'],
  ['02/oct.', '30', '29'],
  ['03/oct.', '30', '29'],
  ['04/oct.', '30', '28'],
];

fdescribe('ChartComponent', () => {
  let component: ChartComponent;
  let fixture: ComponentFixture<ChartComponent>;
  let dom: HTMLElement;
  let location: Location;
  let router: Router;
  let dataSentToGoogleChartDrawMethod: [[], {}];
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ChartComponent, DummyComponent],
      imports: [
        GoogleChartsModule,
        RouterTestingModule,
        RouterTestingModule.withRoutes([
          {
            path: 'chart',
            component: DummyComponent,
          },
          {
            path: 'any-other-page',
            component: DummyComponent,
          },
        ]),
      ],
    }).compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(ChartComponent);
    component = fixture.componentInstance;
    dom = fixture.debugElement.nativeElement;
    router = TestBed.get(Router);
    location = TestBed.get(Location);
    component.chartData = mockChartData;
    component.options = { options: 'custom options' };
    window['google'] = {
      charts: {
        load: function(): void {},
        setOnLoadCallback: function(callback: Function): Observable<any> {
          return Observable.create();
        },
      },
      visualization: {
        arrayToDataTable: function(data: string[][]): string[][] {
          return data;
        },
        LineChart: class {
          dataReceived: any;
          constructor(data: string[][]) {
            this.dataReceived = data;
          }
          draw = function(data: any, options: {}) {
            dataSentToGoogleChartDrawMethod = [data, options];
          };
        },
      },
    };
    fixture.detectChanges();
  });

  it('should mount', () => {
    expect(component).toBeTruthy();
  });

  it('should call a Google Chart draw method onInit', async () => {
    const spySetOnLoadCallback = spyOn(
      google['charts'],
      'setOnLoadCallback',
    ).and.callThrough();
    component.ngOnInit();
    expect(spySetOnLoadCallback).toHaveBeenCalled();
  });

  it('should call Google Chart methods to build charts when drawChart is called', () => {
    component.drawChart(mockChartData);
    expect(dataSentToGoogleChartDrawMethod).toEqual([
      mockChartData,
      { options: 'custom options' },
    ]);
  });

  it('should re-render Chart when window is resized when on /charts page', fakeAsync(() => {
    router.navigate(['/chart']);
    tick();
    const spyOnDrawChart = spyOn(component, 'drawChart');
    expect(location.path()).toBe('/chart');
    window.dispatchEvent(new Event('resize'));
    expect(spyOnDrawChart).toHaveBeenCalled();
  }));

  it('should NOT try to re-render chart when window is resized on other pages than /chart', fakeAsync(() => {
    router.navigate(['/any-other-page']);
    tick();
    const spyOnDrawChart = spyOn(component, 'drawChart');
    expect(location.path()).toBe('/any-other-page');
    window.dispatchEvent(new Event('resize'));
    expect(spyOnDrawChart).not.toHaveBeenCalled();
  }));
});

There is one drawback thought: it leaves a callback function out of test and reaches only 94.44% of coverage.

The part I was not able to test was this.drawChart(this.chartData) fired as callback of google.charts.setOnLoadCallback.

Thus, any help to reach the mentioned callback and achieving a coverage of 100% would be really appreciated. :)

Анна
  • 1,248
  • 15
  • 26
1

You need to make one change to your mock of the google methods, and then that callback will be tested. Change your current code of:

window['google'] = {
  charts: {
    load: function(): void {},
    setOnLoadCallback: function(callback: Function): Observable<any> {
      return Observable.create();
    },
  },

to:

window['google'] = {
  charts: {
    load: function(): void {},
    setOnLoadCallback: callback => callback(),
  },

This will make the mock of setOnLoadCallback() actually call the provided callback.

Then you could write a spec such as the following to be sure it is working:

it('should call the callback() method onInit', () => {
  const callBackSpy = spyOn(component, 'drawChart');
  component.ngOnInit();
  expect(callBackSpy).toHaveBeenCalled();
});

I set this all up in a Stackblitz to show it working.

I hope this helps.

dmcgrandle
  • 5,934
  • 1
  • 19
  • 38
  • I guess initially I was trying something like this simple arrow function, but Karma returned `google.charts. setOnLoadCallback was not a function`. But I guess maybe I was doing something wrong... I can only check it on Monday. Anyway thanks! Stackblitz link provided was very helpful. – Анна Dec 15 '18 at 00:20