]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Cleanup and upgrade unit tests environment
authorSimon Brunel <simonbrunel@users.noreply.github.com>
Sat, 4 Mar 2017 15:47:53 +0000 (16:47 +0100)
committerEvert Timberg <evert.timberg+github@gmail.com>
Sat, 4 Mar 2017 16:39:48 +0000 (11:39 -0500)
`karma.conf.ci.js` has been merged into `karma.conf.js` for local testing consistency: `gulp unittestWatch` has been replaced by `gulp unittest --watch` and thus use exactly the same config file. Upgrade to latest jasmine and karma packages and remove deprecated `gulp-karma` dependency (directly use `karma.Server` in gulp).

Split `test/mockContext.js` into smaller `test/jasmine.*` modules to make easier unit tests maintenance and finally, move all `*.test.js` files under the `test/specs` folder.

39 files changed:
gulpfile.js
karma.conf.ci.js [deleted file]
karma.conf.js
karma.coverage.conf.js
package.json
test/jasmine.context.js [new file with mode: 0644]
test/jasmine.index.js [new file with mode: 0644]
test/jasmine.matchers.js [new file with mode: 0644]
test/jasmine.utils.js [new file with mode: 0644]
test/mockContext.js [deleted file]
test/specs/controller.bar.tests.js [moved from test/controller.bar.tests.js with 100% similarity]
test/specs/controller.bubble.tests.js [moved from test/controller.bubble.tests.js with 100% similarity]
test/specs/controller.doughnut.tests.js [moved from test/controller.doughnut.tests.js with 100% similarity]
test/specs/controller.line.tests.js [moved from test/controller.line.tests.js with 100% similarity]
test/specs/controller.polarArea.tests.js [moved from test/controller.polarArea.tests.js with 100% similarity]
test/specs/controller.radar.tests.js [moved from test/controller.radar.tests.js with 100% similarity]
test/specs/core.controller.tests.js [moved from test/core.controller.tests.js with 100% similarity]
test/specs/core.datasetController.tests.js [moved from test/core.datasetController.tests.js with 100% similarity]
test/specs/core.element.tests.js [moved from test/core.element.tests.js with 100% similarity]
test/specs/core.helpers.tests.js [moved from test/core.helpers.tests.js with 100% similarity]
test/specs/core.interaction.tests.js [moved from test/core.interaction.tests.js with 100% similarity]
test/specs/core.layoutService.tests.js [moved from test/core.layoutService.tests.js with 100% similarity]
test/specs/core.legend.tests.js [moved from test/core.legend.tests.js with 100% similarity]
test/specs/core.plugin.tests.js [moved from test/core.plugin.tests.js with 100% similarity]
test/specs/core.scaleService.tests.js [moved from test/core.scaleService.tests.js with 100% similarity]
test/specs/core.title.tests.js [moved from test/core.title.tests.js with 100% similarity]
test/specs/core.tooltip.tests.js [moved from test/core.tooltip.tests.js with 100% similarity]
test/specs/element.arc.tests.js [moved from test/element.arc.tests.js with 100% similarity]
test/specs/element.line.tests.js [moved from test/element.line.tests.js with 100% similarity]
test/specs/element.point.tests.js [moved from test/element.point.tests.js with 100% similarity]
test/specs/element.rectangle.tests.js [moved from test/element.rectangle.tests.js with 100% similarity]
test/specs/global.defaults.tests.js [moved from test/defaultConfig.tests.js with 100% similarity]
test/specs/global.deprecations.tests.js [moved from test/global.deprecations.tests.js with 100% similarity]
test/specs/platform.dom.tests.js [moved from test/platform.dom.tests.js with 100% similarity]
test/specs/scale.category.tests.js [moved from test/scale.category.tests.js with 100% similarity]
test/specs/scale.linear.tests.js [moved from test/scale.linear.tests.js with 100% similarity]
test/specs/scale.logarithmic.tests.js [moved from test/scale.logarithmic.tests.js with 100% similarity]
test/specs/scale.radialLinear.tests.js [moved from test/scale.radialLinear.tests.js with 100% similarity]
test/specs/scale.time.tests.js [moved from test/scale.time.tests.js with 100% similarity]

index 6cd6efa5d919749ac7a9e7fea23d8b45990e4644..86800169631b1c214488772805f097d46310baa6 100644 (file)
@@ -12,12 +12,13 @@ var uglify = require('gulp-uglify');
 var util = require('gulp-util');
 var zip = require('gulp-zip');
 var exec = require('child_process').exec;
-var karma = require('gulp-karma');
+var karma = require('karma');
 var browserify = require('browserify');
 var source = require('vinyl-source-stream');
 var merge = require('merge-stream');
 var collapse = require('bundle-collapser/plugin');
 var argv  = require('yargs').argv
+var path = require('path');
 var package = require('./package.json');
 
 var srcDir = './src/';
@@ -34,14 +35,6 @@ var header = "/*!\n" +
   " * https://github.com/chartjs/Chart.js/blob/master/LICENSE.md\n" +
   " */\n";
 
-var preTestFiles = [
-  './node_modules/moment/min/moment.min.js'
-];
-
-var testFiles = [
-  './test/*.js'
-];
-
 gulp.task('bower', bowerTask);
 gulp.task('build', buildTask);
 gulp.task('package', packageTask);
@@ -53,7 +46,6 @@ gulp.task('size', ['library-size', 'module-sizes']);
 gulp.task('server', serverTask);
 gulp.task('validHTML', validHTMLTask);
 gulp.task('unittest', unittestTask);
-gulp.task('unittestWatch', unittestWatchTask);
 gulp.task('library-size', librarySizeTask);
 gulp.task('module-sizes', moduleSizesTask);
 gulp.task('_open', _openTask);
@@ -157,6 +149,7 @@ function lintTask() {
       'beforeEach',
       'describe',
       'expect',
+      'fail',
       'it',
       'jasmine',
       'moment',
@@ -177,37 +170,31 @@ function validHTMLTask() {
 }
 
 function startTest() {
-  return [].concat(preTestFiles).concat([
-      './src/**/*.js',
-      './test/mockContext.js'
-    ]).concat(
-      argv.inputs?
-        argv.inputs.split(';'):
-        testFiles);
+  return [
+    './node_modules/moment/min/moment.min.js',
+    './test/jasmine.index.js',
+    './src/**/*.js',
+  ].concat(
+    argv.inputs?
+      argv.inputs.split(';'):
+      ['./test/specs/**/*.js']
+  );
 }
 
-function unittestTask() {
-  return gulp.src(startTest())
-    .pipe(karma({
-      configFile: 'karma.conf.ci.js',
-      action: 'run'
-    }));
+function unittestTask(done) {
+  new karma.Server({
+    configFile: path.join(__dirname, 'karma.conf.js'),
+    singleRun: !argv.watch,
+    files: startTest(),
+  }, done).start();
 }
 
-function unittestWatchTask() {
-  return gulp.src(startTest())
-    .pipe(karma({
-      configFile: 'karma.conf.js',
-      action: 'watch'
-    }));
-}
-
-function coverageTask() {
-  return gulp.src(startTest())
-    .pipe(karma({
-      configFile: 'karma.coverage.conf.js',
-      action: 'run'
-    }));
+function coverageTask(done) {
+  new karma.Server({
+    configFile: path.join(__dirname, 'karma.coverage.conf.js'),
+    files: startTest(),
+    singleRun: true,
+  }, done).start();
 }
 
 function librarySizeTask() {
diff --git a/karma.conf.ci.js b/karma.conf.ci.js
deleted file mode 100644 (file)
index 7ed5d0f..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-module.exports = function(config) {
-       var configuration = {
-               browsers: ['Firefox'],
-               customLaunchers: {
-                       Chrome_travis_ci: {
-                               base: 'Chrome',
-                               flags: ['--no-sandbox']
-                       }
-               },
-               frameworks: ['browserify', 'jasmine'],
-               reporters: ['progress', 'html'],
-               preprocessors: {
-                       'src/**/*.js': ['browserify']
-               },
-               browserify: {
-                       debug: true
-               }
-       };
-
-       if (process.env.TRAVIS) {
-               configuration.browsers.push('Chrome_travis_ci');
-       }
-
-       config.set(configuration);
-};
\ No newline at end of file
index acdff3f9cad2cc5d8d3dbd09c6e2e97a4e2b12b2..39e5ae61abcdd9f4526d3f0bef2902053109ceea 100644 (file)
@@ -1,14 +1,33 @@
-module.exports = function(config) {
-       config.set({
-               browsers: ['Chrome', 'Firefox'],
+/* eslint camelcase: 0 */
+
+module.exports = function(karma) {
+       var config = {
+               browsers: ['Firefox'],
                frameworks: ['browserify', 'jasmine'],
-               reporters: ['progress', 'html'],
+               reporters: ['progress', 'kjhtml'],
 
                preprocessors: {
-                       'src/**/*.js': ['browserify']
+                       './test/jasmine.index.js': ['browserify'],
+                       './src/**/*.js': ['browserify']
                },
+
                browserify: {
                        debug: true
                }
-       });
-};
\ No newline at end of file
+       };
+
+       // https://swizec.com/blog/how-to-run-javascript-tests-in-chrome-on-travis/swizec/6647
+       if (process.env.TRAVIS) {
+               config.browsers.push('chrome_travis_ci');
+               config.customLaunchers = {
+                       chrome_travis_ci: {
+                               base: 'Chrome',
+                               flags: ['--no-sandbox']
+                       }
+               };
+       } else {
+               config.browsers.push('Chrome');
+       }
+
+       karma.set(config);
+};
index dd70f701ac65b61cd71bb0b1207fee1ce196c7c3..4975f1688b24bae432ac07683e16b30bc7d33f0d 100644 (file)
@@ -1,12 +1,14 @@
 module.exports = function(config) {
        var configuration = {
                browsers: ['Firefox'],
-
                frameworks: ['browserify', 'jasmine'],
+               reporters: ['progress', 'coverage'],
 
                preprocessors: {
-                       'src/**/*.js': ['browserify']
+                       './test/jasmine.index.js': ['browserify'],
+                       './src/**/*.js': ['browserify']
                },
+
                browserify: {
                        debug: true,
                        transform: [['browserify-istanbul', {
@@ -15,8 +17,7 @@ module.exports = function(config) {
                                }
                        }]]
                },
-               
-               reporters: ['progress', 'coverage'],
+
                coverageReporter: {
                        dir: 'coverage/',
                        reporters: [
index 6a3a28ea9b29cfa240c18fac7f83e697cd54a3eb..374f90d17a578514e79e73647b287cfbf7d77018 100644 (file)
     "gulp-file": "^0.3.0",
     "gulp-html-validator": "^0.0.2",
     "gulp-insert": "~0.5.0",
-    "gulp-karma": "0.0.4",
     "gulp-replace": "^0.5.4",
     "gulp-size": "~0.4.0",
     "gulp-streamify": "^1.0.2",
     "gulp-uglify": "~2.0.x",
     "gulp-util": "~2.2.x",
     "gulp-zip": "~3.2.0",
-    "jasmine": "^2.3.2",
-    "jasmine-core": "^2.3.4",
-    "karma": "^0.12.37",
-    "karma-browserify": "^5.0.1",
-    "karma-chrome-launcher": "^0.2.0",
-    "karma-coverage": "^0.5.1",
-    "karma-firefox-launcher": "^0.1.6",
-    "karma-jasmine": "^0.3.6",
-    "karma-jasmine-html-reporter": "^0.1.8",
+    "jasmine": "^2.5.0",
+    "jasmine-core": "^2.5.0",
+    "karma": "^1.5.0",
+    "karma-browserify": "^5.1.0",
+    "karma-chrome-launcher": "^2.0.0",
+    "karma-coverage": "^1.1.0",
+    "karma-firefox-launcher": "^1.0.0",
+    "karma-jasmine": "^1.1.0",
+    "karma-jasmine-html-reporter": "^0.2.2",
     "merge-stream": "^1.0.0",
     "vinyl-source-stream": "^1.1.0",
     "watchify": "^3.7.0",
diff --git a/test/jasmine.context.js b/test/jasmine.context.js
new file mode 100644 (file)
index 0000000..814c246
--- /dev/null
@@ -0,0 +1,125 @@
+// Code from http://stackoverflow.com/questions/4406864/html-canvas-unit-testing
+var Context = function() {
+       this._calls = []; // names/args of recorded calls
+       this._initMethods();
+
+       this._fillStyle = null;
+       this._lineCap = null;
+       this._lineDashOffset = null;
+       this._lineJoin = null;
+       this._lineWidth = null;
+       this._strokeStyle = null;
+
+       // Define properties here so that we can record each time they are set
+       Object.defineProperties(this, {
+               fillStyle: {
+                       get: function() {
+                               return this._fillStyle;
+                       },
+                       set: function(style) {
+                               this._fillStyle = style;
+                               this.record('setFillStyle', [style]);
+                       }
+               },
+               lineCap: {
+                       get: function() {
+                               return this._lineCap;
+                       },
+                       set: function(cap) {
+                               this._lineCap = cap;
+                               this.record('setLineCap', [cap]);
+                       }
+               },
+               lineDashOffset: {
+                       get: function() {
+                               return this._lineDashOffset;
+                       },
+                       set: function(offset) {
+                               this._lineDashOffset = offset;
+                               this.record('setLineDashOffset', [offset]);
+                       }
+               },
+               lineJoin: {
+                       get: function() {
+                               return this._lineJoin;
+                       },
+                       set: function(join) {
+                               this._lineJoin = join;
+                               this.record('setLineJoin', [join]);
+                       }
+               },
+               lineWidth: {
+                       get: function() {
+                               return this._lineWidth;
+                       },
+                       set: function(width) {
+                               this._lineWidth = width;
+                               this.record('setLineWidth', [width]);
+                       }
+               },
+               strokeStyle: {
+                       get: function() {
+                               return this._strokeStyle;
+                       },
+                       set: function(style) {
+                               this._strokeStyle = style;
+                               this.record('setStrokeStyle', [style]);
+                       }
+               },
+       });
+};
+
+Context.prototype._initMethods = function() {
+       // define methods to test here
+       // no way to introspect so we have to do some extra work :(
+       var me = this;
+       var methods = {
+               arc: function() {},
+               beginPath: function() {},
+               bezierCurveTo: function() {},
+               clearRect: function() {},
+               closePath: function() {},
+               fill: function() {},
+               fillRect: function() {},
+               fillText: function() {},
+               lineTo: function() {},
+               measureText: function(text) {
+                       // return the number of characters * fixed size
+                       return text ? {width: text.length * 10} : {width: 0};
+               },
+               moveTo: function() {},
+               quadraticCurveTo: function() {},
+               restore: function() {},
+               rotate: function() {},
+               save: function() {},
+               setLineDash: function() {},
+               stroke: function() {},
+               strokeRect: function() {},
+               setTransform: function() {},
+               translate: function() {},
+       };
+
+       Object.keys(methods).forEach(function(name) {
+               me[name] = function() {
+                       me.record(name, arguments);
+                       return methods[name].apply(me, arguments);
+               };
+       });
+};
+
+Context.prototype.record = function(methodName, args) {
+       this._calls.push({
+               name: methodName,
+               args: Array.prototype.slice.call(args)
+       });
+};
+
+Context.prototype.getCalls = function() {
+       return this._calls;
+};
+
+Context.prototype.resetCalls = function() {
+       this._calls = [];
+};
+
+module.exports = Context;
diff --git a/test/jasmine.index.js b/test/jasmine.index.js
new file mode 100644 (file)
index 0000000..aeaee77
--- /dev/null
@@ -0,0 +1,53 @@
+var Context = require('./jasmine.context');
+var matchers = require('./jasmine.matchers');
+var utils = require('./jasmine.utils');
+
+(function() {
+
+       // Keep track of all acquired charts to automatically release them after each specs
+       var charts = {};
+
+       function acquireChart() {
+               var chart = utils.acquireChart.apply(utils, arguments);
+               charts[chart.id] = chart;
+               return chart;
+       }
+
+       function releaseChart(chart) {
+               utils.releaseChart.apply(utils, arguments);
+               delete charts[chart.id];
+       }
+
+       function createMockContext() {
+               return new Context();
+       }
+
+       window.acquireChart = acquireChart;
+       window.releaseChart = releaseChart;
+       window.createMockContext = createMockContext;
+
+       // some style initialization to limit differences between browsers across different plateforms.
+       utils.injectCSS(
+               '.chartjs-wrapper, .chartjs-wrapper canvas {' +
+                       'border: 0;' +
+                       'margin: 0;' +
+                       'padding: 0;' +
+               '}' +
+               '.chartjs-wrapper {' +
+                       'position: absolute' +
+               '}');
+
+       beforeEach(function() {
+               jasmine.addMatchers(matchers);
+       });
+
+       afterEach(function() {
+               // Auto releasing acquired charts
+               Object.keys(charts).forEach(function(id) {
+                       var chart = charts[id];
+                       if (!(chart.$test || {}).persistent) {
+                               releaseChart(chart);
+                       }
+               });
+       });
+}());
diff --git a/test/jasmine.matchers.js b/test/jasmine.matchers.js
new file mode 100644 (file)
index 0000000..abb90d7
--- /dev/null
@@ -0,0 +1,113 @@
+'use strict';
+
+function toBeCloseToPixel() {
+       return {
+               compare: function(actual, expected) {
+                       var result = false;
+
+                       if (!isNaN(actual) && !isNaN(expected)) {
+                               var diff = Math.abs(actual - expected);
+                               var A = Math.abs(actual);
+                               var B = Math.abs(expected);
+                               var percentDiff = 0.005; // 0.5% diff
+                               result = (diff <= (A > B ? A : B) * percentDiff) || diff < 2; // 2 pixels is fine
+                       }
+
+                       return {pass: result};
+               }
+       };
+}
+
+function toEqualOneOf() {
+       return {
+               compare: function(actual, expecteds) {
+                       var result = false;
+                       for (var i = 0, l = expecteds.length; i < l; i++) {
+                               if (actual === expecteds[i]) {
+                                       result = true;
+                                       break;
+                               }
+                       }
+                       return {
+                               pass: result
+                       };
+               }
+       };
+}
+
+function toBeValidChart() {
+       return {
+               compare: function(actual) {
+                       var message = null;
+
+                       if (!(actual instanceof Chart)) {
+                               message = 'Expected ' + actual + ' to be an instance of Chart';
+                       } else if (!(actual.canvas instanceof HTMLCanvasElement)) {
+                               message = 'Expected canvas to be an instance of HTMLCanvasElement';
+                       } else if (!(actual.ctx instanceof CanvasRenderingContext2D)) {
+                               message = 'Expected context to be an instance of CanvasRenderingContext2D';
+                       } else if (typeof actual.height !== 'number' || !isFinite(actual.height)) {
+                               message = 'Expected height to be a strict finite number';
+                       } else if (typeof actual.width !== 'number' || !isFinite(actual.width)) {
+                               message = 'Expected width to be a strict finite number';
+                       }
+
+                       return {
+                               message: message? message : 'Expected ' + actual + ' to be valid chart',
+                               pass: !message
+                       };
+               }
+       };
+}
+
+function toBeChartOfSize() {
+       return {
+               compare: function(actual, expected) {
+                       var res = toBeValidChart().compare(actual);
+                       if (!res.pass) {
+                               return res;
+                       }
+
+                       var message = null;
+                       var canvas = actual.ctx.canvas;
+                       var style = getComputedStyle(canvas);
+                       var pixelRatio = window.devicePixelRatio;
+                       var dh = parseInt(style.height, 10);
+                       var dw = parseInt(style.width, 10);
+                       var rh = canvas.height;
+                       var rw = canvas.width;
+                       var orh = rh / pixelRatio;
+                       var orw = rw / pixelRatio;
+
+                       // sanity checks
+                       if (actual.height !== orh) {
+                               message = 'Expected chart height ' + actual.height + ' to be equal to original render height ' + orh;
+                       } else if (actual.width !== orw) {
+                               message = 'Expected chart width ' + actual.width + ' to be equal to original render width ' + orw;
+                       }
+
+                       // validity checks
+                       if (dh !== expected.dh) {
+                               message = 'Expected display height ' + dh + ' to be equal to ' + expected.dh;
+                       } else if (dw !== expected.dw) {
+                               message = 'Expected display width ' + dw + ' to be equal to ' + expected.dw;
+                       } else if (rh !== expected.rh) {
+                               message = 'Expected render height ' + rh + ' to be equal to ' + expected.rh;
+                       } else if (rw !== expected.rw) {
+                               message = 'Expected render width ' + rw + ' to be equal to ' + expected.rw;
+                       }
+
+                       return {
+                               message: message? message : 'Expected ' + actual + ' to be a chart of size ' + expected,
+                               pass: !message
+                       };
+               }
+       };
+}
+
+module.exports = {
+       toBeCloseToPixel: toBeCloseToPixel,
+       toEqualOneOf: toEqualOneOf,
+       toBeValidChart: toBeValidChart,
+       toBeChartOfSize: toBeChartOfSize
+};
diff --git a/test/jasmine.utils.js b/test/jasmine.utils.js
new file mode 100644 (file)
index 0000000..77f05c6
--- /dev/null
@@ -0,0 +1,76 @@
+/**
+ * Injects a new canvas (and div wrapper) and creates teh associated Chart instance
+ * using the given config. Additional options allow tweaking elements generation.
+ * @param {object} config - Chart config.
+ * @param {object} options - Chart acquisition options.
+ * @param {object} options.canvas - Canvas attributes.
+ * @param {object} options.wrapper - Canvas wrapper attributes.
+ * @param {boolean} options.persistent - If true, the chart will not be released after the spec.
+ */
+function acquireChart(config, options) {
+       var wrapper = document.createElement('div');
+       var canvas = document.createElement('canvas');
+       var chart, key;
+
+       config = config || {};
+       options = options || {};
+       options.canvas = options.canvas || {height: 512, width: 512};
+       options.wrapper = options.wrapper || {class: 'chartjs-wrapper'};
+
+       for (key in options.canvas) {
+               if (options.canvas.hasOwnProperty(key)) {
+                       canvas.setAttribute(key, options.canvas[key]);
+               }
+       }
+
+       for (key in options.wrapper) {
+               if (options.wrapper.hasOwnProperty(key)) {
+                       wrapper.setAttribute(key, options.wrapper[key]);
+               }
+       }
+
+       // by default, remove chart animation and auto resize
+       config.options = config.options || {};
+       config.options.animation = config.options.animation === undefined? false : config.options.animation;
+       config.options.responsive = config.options.responsive === undefined? false : config.options.responsive;
+       config.options.defaultFontFamily = config.options.defaultFontFamily || 'Arial';
+
+       wrapper.appendChild(canvas);
+       window.document.body.appendChild(wrapper);
+
+       chart = new Chart(canvas.getContext('2d'), config);
+       chart.$test = {
+               persistent: options.persistent,
+               wrapper: wrapper
+       };
+
+       return chart;
+}
+
+function releaseChart(chart) {
+       chart.destroy();
+
+       var wrapper = (chart.$test || {}).wrapper;
+       if (wrapper && wrapper.parentNode) {
+               wrapper.parentNode.removeChild(wrapper);
+       }
+}
+
+function injectCSS(css) {
+       // http://stackoverflow.com/q/3922139
+       var head = document.getElementsByTagName('head')[0];
+       var style = document.createElement('style');
+       style.setAttribute('type', 'text/css');
+       if (style.styleSheet) {   // IE
+               style.styleSheet.cssText = css;
+       } else {
+               style.appendChild(document.createTextNode(css));
+       }
+       head.appendChild(style);
+}
+
+module.exports = {
+       injectCSS: injectCSS,
+       acquireChart: acquireChart,
+       releaseChart: releaseChart
+};
diff --git a/test/mockContext.js b/test/mockContext.js
deleted file mode 100644 (file)
index b6d2c13..0000000
+++ /dev/null
@@ -1,348 +0,0 @@
-/* eslint guard-for-in: 1 */
-/* eslint camelcase: 1 */
-(function() {
-       // Code from http://stackoverflow.com/questions/4406864/html-canvas-unit-testing
-       var Context = function() {
-               this._calls = []; // names/args of recorded calls
-               this._initMethods();
-
-               this._fillStyle = null;
-               this._lineCap = null;
-               this._lineDashOffset = null;
-               this._lineJoin = null;
-               this._lineWidth = null;
-               this._strokeStyle = null;
-
-               // Define properties here so that we can record each time they are set
-               Object.defineProperties(this, {
-                       fillStyle: {
-                               get: function() {
-                                       return this._fillStyle;
-                               },
-                               set: function(style) {
-                                       this._fillStyle = style;
-                                       this.record('setFillStyle', [style]);
-                               }
-                       },
-                       lineCap: {
-                               get: function() {
-                                       return this._lineCap;
-                               },
-                               set: function(cap) {
-                                       this._lineCap = cap;
-                                       this.record('setLineCap', [cap]);
-                               }
-                       },
-                       lineDashOffset: {
-                               get: function() {
-                                       return this._lineDashOffset;
-                               },
-                               set: function(offset) {
-                                       this._lineDashOffset = offset;
-                                       this.record('setLineDashOffset', [offset]);
-                               }
-                       },
-                       lineJoin: {
-                               get: function() {
-                                       return this._lineJoin;
-                               },
-                               set: function(join) {
-                                       this._lineJoin = join;
-                                       this.record('setLineJoin', [join]);
-                               }
-                       },
-                       lineWidth: {
-                               get: function() {
-                                       return this._lineWidth;
-                               },
-                               set: function(width) {
-                                       this._lineWidth = width;
-                                       this.record('setLineWidth', [width]);
-                               }
-                       },
-                       strokeStyle: {
-                               get: function() {
-                                       return this._strokeStyle;
-                               },
-                               set: function(style) {
-                                       this._strokeStyle = style;
-                                       this.record('setStrokeStyle', [style]);
-                               }
-                       },
-               });
-       };
-
-       Context.prototype._initMethods = function() {
-               // define methods to test here
-               // no way to introspect so we have to do some extra work :(
-               var methods = {
-                       arc: function() {},
-                       beginPath: function() {},
-                       bezierCurveTo: function() {},
-                       clearRect: function() {},
-                       closePath: function() {},
-                       fill: function() {},
-                       fillRect: function() {},
-                       fillText: function() {},
-                       lineTo: function() {},
-                       measureText: function(text) {
-                               // return the number of characters * fixed size
-                               return text ? {width: text.length * 10} : {width: 0};
-                       },
-                       moveTo: function() {},
-                       quadraticCurveTo: function() {},
-                       restore: function() {},
-                       rotate: function() {},
-                       save: function() {},
-                       setLineDash: function() {},
-                       stroke: function() {},
-                       strokeRect: function() {},
-                       setTransform: function() {},
-                       translate: function() {},
-               };
-
-               // attach methods to the class itself
-               var me = this;
-               var methodName;
-
-               var addMethod = function(name, method) {
-                       me[methodName] = function() {
-                               me.record(name, arguments);
-                               return method.apply(me, arguments);
-                       };
-               };
-
-               for (methodName in methods) {
-                       var method = methods[methodName];
-
-                       addMethod(methodName, method);
-               }
-       };
-
-       Context.prototype.record = function(methodName, args) {
-               this._calls.push({
-                       name: methodName,
-                       args: Array.prototype.slice.call(args)
-               });
-       };
-
-       Context.prototype.getCalls = function() {
-               return this._calls;
-       };
-
-       Context.prototype.resetCalls = function() {
-               this._calls = [];
-       };
-
-       window.createMockContext = function() {
-               return new Context();
-       };
-
-       // Custom matcher
-       function toBeCloseToPixel() {
-               return {
-                       compare: function(actual, expected) {
-                               var result = false;
-
-                               if (!isNaN(actual) && !isNaN(expected)) {
-                                       var diff = Math.abs(actual - expected);
-                                       var A = Math.abs(actual);
-                                       var B = Math.abs(expected);
-                                       var percentDiff = 0.005; // 0.5% diff
-                                       result = (diff <= (A > B ? A : B) * percentDiff) || diff < 2; // 2 pixels is fine
-                               }
-
-                               return {pass: result};
-                       }
-               };
-       }
-
-       function toEqualOneOf() {
-               return {
-                       compare: function(actual, expecteds) {
-                               var result = false;
-                               for (var i = 0, l = expecteds.length; i < l; i++) {
-                                       if (actual === expecteds[i]) {
-                                               result = true;
-                                               break;
-                                       }
-                               }
-                               return {
-                                       pass: result
-                               };
-                       }
-               };
-       }
-
-       function toBeValidChart() {
-               return {
-                       compare: function(actual) {
-                               var message = null;
-
-                               if (!(actual instanceof Chart)) {
-                                       message = 'Expected ' + actual + ' to be an instance of Chart';
-                               } else if (!(actual.canvas instanceof HTMLCanvasElement)) {
-                                       message = 'Expected canvas to be an instance of HTMLCanvasElement';
-                               } else if (!(actual.ctx instanceof CanvasRenderingContext2D)) {
-                                       message = 'Expected context to be an instance of CanvasRenderingContext2D';
-                               } else if (typeof actual.height !== 'number' || !isFinite(actual.height)) {
-                                       message = 'Expected height to be a strict finite number';
-                               } else if (typeof actual.width !== 'number' || !isFinite(actual.width)) {
-                                       message = 'Expected width to be a strict finite number';
-                               }
-
-                               return {
-                                       message: message? message : 'Expected ' + actual + ' to be valid chart',
-                                       pass: !message
-                               };
-                       }
-               };
-       }
-
-       function toBeChartOfSize() {
-               return {
-                       compare: function(actual, expected) {
-                               var res = toBeValidChart().compare(actual);
-                               if (!res.pass) {
-                                       return res;
-                               }
-
-                               var message = null;
-                               var canvas = actual.ctx.canvas;
-                               var style = getComputedStyle(canvas);
-                               var pixelRatio = window.devicePixelRatio;
-                               var dh = parseInt(style.height, 10);
-                               var dw = parseInt(style.width, 10);
-                               var rh = canvas.height;
-                               var rw = canvas.width;
-                               var orh = rh / pixelRatio;
-                               var orw = rw / pixelRatio;
-
-                               // sanity checks
-                               if (actual.height !== orh) {
-                                       message = 'Expected chart height ' + actual.height + ' to be equal to original render height ' + orh;
-                               } else if (actual.width !== orw) {
-                                       message = 'Expected chart width ' + actual.width + ' to be equal to original render width ' + orw;
-                               }
-
-                               // validity checks
-                               if (dh !== expected.dh) {
-                                       message = 'Expected display height ' + dh + ' to be equal to ' + expected.dh;
-                               } else if (dw !== expected.dw) {
-                                       message = 'Expected display width ' + dw + ' to be equal to ' + expected.dw;
-                               } else if (rh !== expected.rh) {
-                                       message = 'Expected render height ' + rh + ' to be equal to ' + expected.rh;
-                               } else if (rw !== expected.rw) {
-                                       message = 'Expected render width ' + rw + ' to be equal to ' + expected.rw;
-                               }
-
-                               return {
-                                       message: message? message : 'Expected ' + actual + ' to be a chart of size ' + expected,
-                                       pass: !message
-                               };
-                       }
-               };
-       }
-
-       beforeEach(function() {
-               jasmine.addMatchers({
-                       toBeCloseToPixel: toBeCloseToPixel,
-                       toEqualOneOf: toEqualOneOf,
-                       toBeValidChart: toBeValidChart,
-                       toBeChartOfSize: toBeChartOfSize
-               });
-       });
-
-       // Canvas injection helpers
-       var charts = {};
-
-       /**
-        * Injects a new canvas (and div wrapper) and creates teh associated Chart instance
-        * using the given config. Additional options allow tweaking elements generation.
-        * @param {object} config - Chart config.
-        * @param {object} options - Chart acquisition options.
-        * @param {object} options.canvas - Canvas attributes.
-        * @param {object} options.wrapper - Canvas wrapper attributes.
-        * @param {boolean} options.persistent - If true, the chart will not be released after the spec.
-        */
-       function acquireChart(config, options) {
-               var wrapper = document.createElement('div');
-               var canvas = document.createElement('canvas');
-               var chart, key;
-
-               config = config || {};
-               options = options || {};
-               options.canvas = options.canvas || {height: 512, width: 512};
-               options.wrapper = options.wrapper || {class: 'chartjs-wrapper'};
-
-               for (key in options.canvas) {
-                       if (options.canvas.hasOwnProperty(key)) {
-                               canvas.setAttribute(key, options.canvas[key]);
-                       }
-               }
-
-               for (key in options.wrapper) {
-                       if (options.wrapper.hasOwnProperty(key)) {
-                               wrapper.setAttribute(key, options.wrapper[key]);
-                       }
-               }
-
-               // by default, remove chart animation and auto resize
-               config.options = config.options || {};
-               config.options.animation = config.options.animation === undefined? false : config.options.animation;
-               config.options.responsive = config.options.responsive === undefined? false : config.options.responsive;
-               config.options.defaultFontFamily = config.options.defaultFontFamily || 'Arial';
-
-               wrapper.appendChild(canvas);
-               window.document.body.appendChild(wrapper);
-
-               chart = new Chart(canvas.getContext('2d'), config);
-               chart._test_persistent = options.persistent;
-               chart._test_wrapper = wrapper;
-               charts[chart.id] = chart;
-               return chart;
-       }
-
-       function releaseChart(chart) {
-               chart.destroy();
-               chart._test_wrapper.remove();
-               delete charts[chart.id];
-       }
-
-       afterEach(function() {
-               // Auto releasing acquired charts
-               for (var id in charts) {
-                       var chart = charts[id];
-                       if (!chart._test_persistent) {
-                               releaseChart(chart);
-                       }
-               }
-       });
-
-       function injectCSS(css) {
-               // http://stackoverflow.com/q/3922139
-               var head = document.getElementsByTagName('head')[0];
-               var style = document.createElement('style');
-               style.setAttribute('type', 'text/css');
-               if (style.styleSheet) {   // IE
-                       style.styleSheet.cssText = css;
-               } else {
-                       style.appendChild(document.createTextNode(css));
-               }
-               head.appendChild(style);
-       }
-
-       window.acquireChart = acquireChart;
-       window.releaseChart = releaseChart;
-
-       // some style initialization to limit differences between browsers across different plateforms.
-       injectCSS(
-               '.chartjs-wrapper, .chartjs-wrapper canvas {' +
-                       'border: 0;' +
-                       'margin: 0;' +
-                       'padding: 0;' +
-               '}' +
-               '.chartjs-wrapper {' +
-                       'position: absolute' +
-               '}');
-}());