mirror of
https://github.com/chartjs/Chart.js.git
synced 2024-10-06 20:29:12 +02:00
766ca49cd0
* Add eslint to test files * Fix mockContext for tests * Make formatting look better for nested objects
349 lines
9.2 KiB
JavaScript
349 lines
9.2 KiB
JavaScript
/* 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 chart = actual && actual.chart;
|
|
var message = null;
|
|
|
|
if (!(actual instanceof Chart.Controller)) {
|
|
message = 'Expected ' + actual + ' to be an instance of Chart.Controller';
|
|
} else if (!(chart instanceof Chart)) {
|
|
message = 'Expected chart to be an instance of Chart';
|
|
} else if (!(chart.canvas instanceof HTMLCanvasElement)) {
|
|
message = 'Expected canvas to be an instance of HTMLCanvasElement';
|
|
} else if (!(chart.ctx instanceof CanvasRenderingContext2D)) {
|
|
message = 'Expected context to be an instance of CanvasRenderingContext2D';
|
|
} else if (typeof chart.height !== 'number' || !isFinite(chart.height)) {
|
|
message = 'Expected height to be a strict finite number';
|
|
} else if (typeof chart.width !== 'number' || !isFinite(chart.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 chart = actual.chart;
|
|
var canvas = chart.ctx.canvas;
|
|
var style = getComputedStyle(canvas);
|
|
var dh = parseInt(style.height, 10);
|
|
var dw = parseInt(style.width, 10);
|
|
var rh = canvas.height;
|
|
var rw = canvas.width;
|
|
|
|
// sanity checks
|
|
if (chart.height !== rh) {
|
|
message = 'Expected chart height ' + chart.height + ' to be equal to render height ' + rh;
|
|
} else if (chart.width !== rw) {
|
|
message = 'Expected chart width ' + chart.width + ' to be equal to render width ' + rw;
|
|
}
|
|
|
|
// 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;
|
|
|
|
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' +
|
|
'}');
|
|
}());
|