Chart.js/test/jasmine.matchers.js
Simon Brunel 1ca0ffb5d5 Introduce unit test based on image comparison (#3988)
Attempt to make easier the creation of unit tests that check the drawing output. Until now, this was done by checking calls on a 'fake' context, which is hard to maintain (need to update pixel values by hands) and also not reliable when optimizing code (i.e. different calls sequence but same result).

As of now, it's possible to define 'auto' tests based on JSON/PNG fixtures: chart is generated from the JSON file and compared to the associated PNG image. The image diff is done using `pixelmatch`. As an example (and in preparation of the `filler` plugin), add auto tests for the line element `fill` options.
2017-03-05 09:49:12 -07:00

206 lines
5.7 KiB
JavaScript

'use strict';
var pixelmatch = require('pixelmatch');
var utils = require('./jasmine.utils');
function toPercent(value) {
return Math.round(value * 10000) / 100;
}
function createImageData(w, h) {
var canvas = utils.createCanvas(w, h);
var context = canvas.getContext('2d');
return context.getImageData(0, 0, w, h);
}
function canvasFromImageData(data) {
var canvas = utils.createCanvas(data.width, data.height);
var context = canvas.getContext('2d');
context.putImageData(data, 0, 0);
return canvas;
}
function buildPixelMatchPreview(actual, expected, diff, threshold, tolerance, count) {
var ratio = count / (actual.width * actual.height);
var wrapper = document.createElement('div');
wrapper.style.cssText = 'display: flex; overflow-y: auto';
[
{data: actual, label: 'Actual'},
{data: expected, label: 'Expected'},
{data: diff, label:
'diff: ' + count + 'px ' +
'(' + toPercent(ratio) + '%)<br/>' +
'thr: ' + toPercent(threshold) + '%, ' +
'tol: '+ toPercent(tolerance) + '%'
}
].forEach(function(values) {
var item = document.createElement('div');
item.style.cssText = 'text-align: center; font: 12px monospace; line-height: 1.4; margin: 8px';
item.innerHTML = '<div style="margin: 8px; height: 32px">' + values.label + '</div>';
item.appendChild(canvasFromImageData(values.data));
wrapper.appendChild(item);
});
// WORKAROUND: https://github.com/karma-runner/karma-jasmine/issues/139
wrapper.indexOf = function() {
return -1;
};
return wrapper;
}
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
};
}
};
}
function toEqualImageData() {
return {
compare: function(actual, expected, opts) {
var message = null;
var debug = opts.debug || false;
var tolerance = opts.tolerance === undefined? 0.001 : opts.tolerance;
var threshold = opts.threshold === undefined? 0.1 : opts.threshold;
var ctx, idata, ddata, w, h, count, ratio;
if (actual instanceof Chart) {
ctx = actual.ctx;
} else if (actual instanceof HTMLCanvasElement) {
ctx = actual.getContext('2d');
} else if (actual instanceof CanvasRenderingContext2D) {
ctx = actual;
}
if (ctx) {
h = expected.height;
w = expected.width;
idata = ctx.getImageData(0, 0, w, h);
ddata = createImageData(w, h);
count = pixelmatch(idata.data, expected.data, ddata.data, w, h, {threshold: threshold});
ratio = count / (w * h);
if ((ratio > tolerance) || debug) {
message = buildPixelMatchPreview(idata, expected, ddata, threshold, tolerance, count);
}
} else {
message = 'Input value is not a valid image source.';
}
return {
message: message,
pass: !message
};
}
};
}
module.exports = {
toBeCloseToPixel: toBeCloseToPixel,
toEqualOneOf: toEqualOneOf,
toBeValidChart: toBeValidChart,
toBeChartOfSize: toBeChartOfSize,
toEqualImageData: toEqualImageData
};