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 :)
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.