Search code examples
angularjsunit-testingcanvasjs

AngularJS + CanvasJS: how to unit test?


I'm tring to run unit tests in an AngularJS application scaffolded using Yeoman. The only test I'm trying to run just check if and array has correct length but fails because of something related to CanvasJS

Here the LOG

LOG: 'CanvasJS Error: Chart Container with id "chartContainer" was not found'
PhantomJS 1.9.7 (Windows 7) Controller: MainCtrl should attach a list of inputs to the scope  FAILED
        TypeError: 'undefined' is not an object (evaluating 'this._toolBar.appendChild')
            at /path/to/Chart/app/scripts/vendor/canvasjs-1.5.0-beta/source/canvasjs.js:1799
            at /path/to/Chart/app/scripts/vendor/canvasjs-1.5.0-beta/source/canvasjs.js:1919
            at /path/to/Chart/app/scripts/vendor/canvasjs-1.5.0-beta/source/canvasjs.js:2093
            at /path/to/Chart/app/scripts/vendor/canvasjs-1.5.0-beta/source/canvasjs.js:12551
            at /path/to/Chart/app/scripts/controllers/main.js:101
            at invoke (/path/to/Chart/app/bower_components/angular/angular.js:3869)
            at instantiate (/path/to/Chart/app/bower_components/angular/angular.js:3880)
            at /path/to/Chart/app/bower_components/angular/angular.js:7134
            at /path/to/Chart/test/spec/controllers/main.js:15
            at invoke (/path/to/Chart/app/bower_components/angular/angular.js:3869)
            at workFn (/path/to/Chart/app/bower_components/angular-mocks/angular-mocks.js:2147)
        undefined
        Expected 0 to be 3.
PhantomJS 1.9.7 (Windows 7): Executed 1 of 1 (1 FAILED) ERROR (0.025 secs / 0.021 secs)
Warning: Task "karma:unit" failed. Use --force to continue.

The application can be found here https://github.com/kaiohken1982/Chart

I'm not sure if I have implemented right this library within the AngularJS app, advices will be very appreciated.

Thanks

------------------- EDIT #1 --------------------

Here below some code to make understanding better the scenario, as you can see CanvasJS is used as global not injected as a service, so one of the question is if it is correct to use a third party library this way if there's not a bower repo.

This line I think is causing the issue

...
$scope.chart = new CanvasJS.Chart('chartContainer', {
...

The controller view contains the "chartContainer" id

<div id="chartContainer"></div>

app/scripts/controllers/main.js 'use strict';

/*global CanvasJS:false */

/**
 * @ngdoc function
 * @name ngprojectApp.controller:MainCtrl
 * @description
 * # MainCtrl
 * Controller of the ngprojectApp
 */
angular.module('ChartApp')
  .controller('MainCtrl', function ($scope) {

    // Regexp to ensure that input dates are in the required format
    $scope.dateRegexp = /^(January|February|March|April|June|July|August|Semptember|October|November|December)[ ](0[1-9]|1[0-9]|2[0-9]|3[0-1])[ ](19|20)\d{2}$/i;

    // Called to remove an entry
    $scope.removeInput = function (index) {
      $scope.inputs.splice(index, 1);
    };

    // Called to add an entry
    $scope.addRow = function () {
      $scope.inputs.push($scope.value);
      $scope.value = '';
    };

     $scope.predicate = '';
     $scope.reverse = false;
     $scope.inputs = [];
     $scope.updateMyText = function() {};

     // Chart below
     var datapoints = [];

     var chartPoint = null;
     $scope.canEdit = true;
     $scope.editMode = false;
     $scope.units = 1; // increase or decrease bar value by this unit
     $scope.chart = new CanvasJS.Chart('chartContainer', {
       theme: 'theme1',
       interactivityEnabled: true,
       title:{
          text: 'Website response time'              
       },
       axisY: {
          title: 'ms',
          labelFontSize: 16,
       },
       axisX: {
          title: 'timeline',
          labelFontSize: 16,
          gridThickness: 1
       },
       data: [              
          {
            click: function() {
              $scope.$apply(function() {
                if($scope.canEdit) {
                  $scope.editMode = !$scope.editMode; 
                }
              });

              if($scope.editMode) {
                console.log('EditMode is now TRUE');
              }

              if(!$scope.editMode) {
                console.log('EditMode is now FALSE');
              }
            },
            mousemove: function(e) {
              if(!$scope.editMode) {
                chartPoint = null;
                return false;
              }

              // First point? Assign it and return
              if(null === chartPoint) {
                chartPoint = e;
                return false;
              }

              // Update inputs at the correct index will re-render graph and table data
              $scope.$apply(function() {
                var diffY = e.y - chartPoint.y; // if it is > 0 means that the mouse pointer went UP
                var time = diffY < 0 ? e.dataPoint.y+$scope.units : e.dataPoint.y-$scope.units;
                $scope.inputs[e.dataPointIndex].time = time;
                // Update chartPoint
                chartPoint = e;
              });

            },
             type: 'column',
             dataPoints: []
          }
       ]
    });

    $scope.chart.render(); //render the chart for the first time

    $scope.changeChartType = function(chartType) {
      $scope.canEdit = 'column' === chartType;
      $scope.editMode = false; // disable EditMode whatever the type is
      $scope.chart.options.data[0].dataPoints = datapoints;
      $scope.chart.options.data[0].type = chartType;
        $scope.chart.render(); //re-render the chart to display the new layout
    };

    // DEEP watch for any changes in the array
    $scope.$watch('inputs', function() {
      datapoints = [];
      for(var i in $scope.inputs) {
        var e = $scope.inputs[i];
        // update the timestamp for the value
        var d = new Date(e.date);
        e.timestamp = d.valueOf() / 1000;
        datapoints.push({
          'label': e.url,
          x: new Date(e.date),
          y: e.time
        });
      }
      // Reassign data to chart
      $scope.chart.options.data[0].dataPoints = datapoints;
      $scope.chart.render();
    }, true);


    $scope.inputs.push({
      date: 'January 02 2014',
      url: 'http://uno.it',
      time: 111
    });

    $scope.inputs.push({
      date: 'January 12 2014',
      url: 'http://due.it',
      time: 292
    });

    $scope.inputs.push({
      date: 'January 22 2014',
      url: 'http://tre.it',
      time: 333
    });

  });

test/spec/controllers/main.js 'use strict';

describe('Controller: MainCtrl', function () {

  // load the controller's module
  beforeEach(module('ChartApp'));

  var MainCtrl,
    scope;

  // Initialize the controller and a mock scope
  beforeEach(inject(function ($controller, $rootScope) {
    scope = $rootScope.$new();
    MainCtrl = $controller('MainCtrl', {
      $scope: scope
    });
  }));

  it('should attach a list of inputs to the scope ', function () {
    expect(scope.inputs.length).toBe(3);
  });
});

Karma.conf.js has all required files, I suppose...

....
    files: [
      'app/bower_components/angular/angular.js',
      'app/bower_components/angular-mocks/angular-mocks.js',
      'app/bower_components/angular-animate/angular-animate.js',
      'app/bower_components/angular-cookies/angular-cookies.js',
      'app/bower_components/angular-resource/angular-resource.js',
      'app/bower_components/angular-route/angular-route.js',
      'app/bower_components/angular-sanitize/angular-sanitize.js',
      'app/bower_components/angular-touch/angular-touch.js',

      'app/bower_components/jquery/dist/jquery.js',
      'app/bower_components/angular-ui/build/angular-ui.js',
      'app/bower_components/jquery-ui/jquery-ui.js',
      'app/scripts/vendor/canvasjs-1.5.0-beta/canvasjs.min.js',

      'app/scripts/directives/directive.js',
      'app/scripts/**/*.js',
      'test/mock/**/*.js',
      'test/spec/**/*.js'
    ],
...

------------------- EDIT #2 --------------------

This is how I've edited the main.js test after the advices received

  beforeEach(inject(function ($controller, $rootScope, $compile) {
    var htmlString = '' +
    '<div ng-controller="MainCtrl">' +
    '    <div id="chartContainer">' +
    '</div>'
    ;
    var element = angular.element(htmlString)
    scope = $rootScope.$new();
    $compile(element)(scope);
    scope.$digest();
    MainCtrl = $controller('MainCtrl', {
      $scope: scope
    });
  }));

The error is almost the same

LOG: 'CanvasJS Error: Chart Container with id "chartContainer" was not found'
PhantomJS 1.9.7 (Windows 7) Controller: MainCtrl should attach a list of inputs to the scope  FAILED
        TypeError: 'undefined' is not an object (evaluating 'this._toolBar.appendChild')
            at /path/to/workspace/Chart/app/scripts/vendor/canvasjs-1.5.0-beta/source/canvasjs.js:1799
            at /path/to/workspace/Chart/app/scripts/vendor/canvasjs-1.5.0-beta/source/canvasjs.js:1919
            at /path/to/workspace/Chart/app/scripts/vendor/canvasjs-1.5.0-beta/source/canvasjs.js:2093
            at /path/to/workspace/Chart/app/scripts/vendor/canvasjs-1.5.0-beta/source/canvasjs.js:12551
            at /path/to/workspace/Chart/app/scripts/controllers/main.js:101
            at invoke (/path/to/workspace/Chart/app/bower_components/angular/angular.js:3869)
            at instantiate (/path/to/workspace/Chart/app/bower_components/angular/angular.js:3880)
            at /path/to/workspace/Chart/app/bower_components/angular/angular.js:7134
            at /path/to/workspace/Chart/app/bower_components/angular/angular.js:6538
            at forEach (/path/to/workspace/Chart/app/bower_components/angular/angular.js:330)
            at nodeLinkFn (/path/to/workspace/Chart/app/bower_components/angular/angular.js:6552)
            at compositeLinkFn (/path/to/workspace/Chart/app/bower_components/angular/angular.js:5986)
            at publicLinkFn (/path/to/workspace/Chart/app/bower_components/angular/angular.js:5891)
            at /path/to/workspace/Chart/test/spec/controllers/main.js:20
            at invoke (/path/to/workspace/Chart/app/bower_components/angular/angular.js:3869)
            at workFn (/path/to/workspace/Chart/app/bower_components/angular-mocks/angular-mocks.js:2147)
        undefined
        TypeError: 'undefined' is not an object (evaluating 'scope.inputs.length')
            at /path/to/workspace/Chart/test/spec/controllers/main.js:28
PhantomJS 1.9.7 (Windows 7): Executed 1 of 1 (1 FAILED) ERROR (0.029 secs / 0.028 secs)
Warning: Task "karma:unit" failed. Use --force to continue.

Did I mistake something when update the test controller?

------------------- EDIT #3 --------------------

With the precious help of Ivarni I've ended up with this solution:

In the application controller

...
$scope.chart = new $window.CanvasJS.Chart('chartContainer', {
...

In the controller test

...
beforeEach(inject(function ($controller, $rootScope, $compile, $window) {
    $window.CanvasJS = { Chart: function(){
        this.render = function() {};
    } };
...

CanvasJS is now mocked and UnitTest can run :)


Solution

  • I think your assumption about what's causing the error is correct. Your controller is not attached to a DOM so it's reasonable that it's not being able to locate anything with an id "chartContainer".

    For unit testing this I would try the same approach as I would use to unit test a directive. That is something along the lines of

    beforeEach(inject(function($rootScope, $compile) {
        var htmlString = '' +
            '<div ng-controller="MainCtrl">' +
            '    <div id="chartContainer">' +
            '</div>'
            ;
        element = angular.element(htmlString)
        scope = $rootScope.$new();
        $compile(element)(scope);
        scope.$digest();
    }));
    

    This compiles a minimal piece of DOM with your controller attached and an element with the id required by CanvasJS. You'll probably have to fiddle a bit more with that. I don't see a problem with using CanvasJS as a global as long as you make sure it's included in the karma config.

    An alternative path to explore is to look at Angular's e2e testing functionality. There's a bit of documentation here. That might be a better fit when you're testing DOM rendering.