forked from highcharts/highcharts
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathkarma-setup.js
682 lines (610 loc) · 22.8 KB
/
karma-setup.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
/* eslint-env browser */
/* eslint-disable */
/* global __karma__, Highcharts, Promise, QUnit */
/**
* This file runs in the browser as setup for the karma tests.
*/
var VERBOSE = false;
var CANVAS_WIDTH = 600;
var CANVAS_HEIGHT = 400;
var div;
if (!document.getElementById('container')) {
div = document.createElement('div');
div.setAttribute('id', 'container');
document.body.appendChild(div);
}
if (!document.getElementById('output')) {
div = document.createElement('div');
div.setAttribute('id', 'output');
document.body.appendChild(div);
}
var demoHTML = document.createElement('div');
demoHTML.setAttribute('id', 'demo-html');
document.body.appendChild(demoHTML);
var currentTests = [];
Highcharts.useSerialIds(true);
// Disable animation over all.
Highcharts.setOptions({
chart: {
animation: false
},
plotOptions: {
series: {
animation: false,
kdNow: true,
dataLabels: {
defer: false
},
states: {
hover: {
animation: false
},
select: {
animation: false
},
inactive: {
animation: false
},
normal: {
animation: false
}
},
label: {
// Disable it to avoid diff. Consider enabling it in the future,
// then it can be enabled in the clean-up commit right after a
// release.
enabled: false
}
},
// We cannot use it in plotOptions.series because treemap
// has the same layout option: layoutAlgorithm.
networkgraph: {
layoutAlgorithm: {
enableSimulation: false,
maxIterations: 10
}
},
packedbubble: {
layoutAlgorithm: {
enableSimulation: false,
maxIterations: 10
}
}
},
// Stock's Toolbar decreases width of the chart. At the same time, some
// tests have hardcoded x/y positions for events which cuases them to fail.
// For these tests, let's disable stockTools.gui globally.
stockTools: {
gui: {
enabled: false
}
},
tooltip: {
animation: false
}
});
// Save default functions from the default options, as they are not stringified
// to JSON
/*
function handleDefaultOptionsFunctions(save) {
var defaultOptionsFunctions = {};
function saveDefaultOptionsFunctions(original, path) {
Highcharts.objectEach(original, function (value, key) {
if (
Highcharts.isObject(value, true) &&
!Highcharts.isClass(value) &&
!Highcharts.isDOMElement(value)
) {
// Recurse
saveDefaultOptionsFunctions(original[key], (path ? path + '.' : '') + key);
} else if (save && typeof value === 'function') {
defaultOptionsFunctions[path + '.' + key] = value;
} else if ( // restore
!save &&
typeof value === 'function'
) {
console.log('restore', path + '.' + key)
original[key] = defaultOptionsFunctions[path + '.' + key];
}
});
}
saveDefaultOptionsFunctions(Highcharts.defaultOptions, '');
}
handleDefaultOptionsFunctions(true);
*/
Highcharts.defaultOptionsRaw = JSON.stringify(Highcharts.defaultOptions);
Highcharts.callbacksRaw = Highcharts.Chart.prototype.callbacks.slice(0);
// Override Highcharts and jQuery ajax functions to load from local
function ajax(proceed, attr) {
var success = attr.success;
attr.error = function (e) {
throw new Error('Failed to load: ' + attr.url);
};
if (attr.url && window.JSONSources[attr.url]) {
success.call(attr, window.JSONSources[attr.url]);
} else {
console.log('@ajax: Loading over network', attr.url);
attr.success = function (data) {
window.JSONSources[attr.url] = data;
success.call(this, data);
};
return proceed.call(this, attr);
}
}
Highcharts.wrap(Highcharts.HttpUtilities, 'ajax', ajax);
Highcharts.wrap(Highcharts, 'ajax', ajax);
if (window.$) {
$.getJSON = function (url, callback) { // eslint-disable-line no-undef
callback(window.JSONSources[url]);
};
}
function resetDefaultOptions(testName) {
var defaultOptionsRaw = JSON.parse(Highcharts.defaultOptionsRaw);
// Before running setOptions, delete properties that are undefined by
// default. For example, in `highcharts/members/setoptions`, properties like
// chart.borderWidth and chart.plotBorderWidth are set. The default options
// don't contain these props, so a simple merge won't remove them.
function deleteAddedProperties(copy, original) {
Highcharts.objectEach(copy, function (value, key) {
if (
Highcharts.isObject(value, true) &&
Highcharts.isObject(original[key], true) &&
!Highcharts.isClass(value) &&
!Highcharts.isDOMElement(value)
) {
// Recurse
deleteAddedProperties(copy[key], original[key]);
} else if (
// functions are not saved in defaultOptionsRaw
typeof value !== 'function' &&
!(key in original)
) {
delete copy[key];
}
});
}
deleteAddedProperties(Highcharts.defaultOptions, defaultOptionsRaw);
// Delete functions (not automated as they are not serialized in JSON)
delete Highcharts.defaultOptions.global.getTimezoneOffset;
delete Highcharts.defaultOptions.time.getTimezoneOffset;
Highcharts.setOptions(defaultOptionsRaw);
// Create a new Time instance to avoid state leaks related to time and the
// legacy global options
Highcharts.time = new Highcharts.Time(Highcharts.merge(
Highcharts.defaultOptions.global,
Highcharts.defaultOptions.time
));
}
// Handle wrapping, reset functions that are wrapped in the visual samples to
// prevent the wraps from piling up downstream.
var origWrap = Highcharts.wrap;
var wrappedFunctions = [];
var origAddEvent = Highcharts.addEvent;
var addedEvents = [];
if (window.QUnit) {
// Fix the number localization in IE
if (
/msie/.test(navigator.userAgent) &&
!Number.prototype._toString
) {
Number.prototype._toString = Number.prototype.toString;
Number.prototype.toString = function(radix) {
if (radix) {
return Number.prototype._toString.apply(this, arguments);
} else {
return this.toLocaleString('en', { useGrouping: false, maximumFractionDigits: 20 });
}
}
}
//QUnit.config.seed = 'vaolebrok';
/*
* Compare numbers taking in account an error.
* http://bumbu.me/comparing-numbers-approximately-in-qunitjs/
*
* @param {Float} number
* @param {Float} expected
* @param {Float} error Optional
* @param {String} message Optional
*/
QUnit.assert.close = function (number, expected, error, message) {
// Remove fix of number localization in IE
if (
/msie/.test(navigator.userAgent) &&
Number.prototype._toString
) {
Number.prototype.toString = Number.prototype._toString;
delete Number.prototype._toString;
}
if (error === void 0 || error === null) {
error = 0.00001; // default error
}
var result = number === expected || (number <= expected + error && number >= expected - error) || false;
this.pushResult({
result: result,
actual: number,
expected: expected,
message: message
});
};
QUnit.module('Highcharts', {
beforeEach: function (test) {
if (VERBOSE) {
console.log('Start "' + test.test.testName + '"');
}
currentTests.push(test.test.testName);
// Reset container size that some tests may have modified
var containerStyle = document.getElementById('container').style;
containerStyle.width = 'auto';
containerStyle.height = 'auto';
containerStyle.position = 'absolute';
containerStyle.left = '8';
containerStyle.top = '8';
containerStyle.zIndex = '9999';
// Reset randomizer
Math.randomCursor = 0;
// Wrap the wrap function
Highcharts.wrap = function (ob, prop, fn) {
// Push original function
wrappedFunctions.push([ob, prop, ob[prop]]);
origWrap(ob, prop, fn);
};
// Wrap the addEvent function
Highcharts.addEvent = function (el, type, fn, options) {
var unbinder = origAddEvent(el, type, fn, options);
if (typeof el === 'function' && el.prototype) {
addedEvents.push(unbinder);
}
return unbinder;
}
},
afterEach: function (test) {
if (VERBOSE) {
console.log('- end "' + test.test.testName + '"');
}
currentTests.splice(
currentTests.indexOf(test.test.testName),
1
);
var defaultOptions = JSON.stringify(Highcharts.defaultOptions);
if (defaultOptions !== Highcharts.defaultOptionsRaw) {
//var msg = 'Default options changed, make sure the test resets options';
//console.log(test.test.testName, msg);
//QUnit.config.queue.length = 0;
//throw new Error(msg);
}
var containerStyle = document.getElementById('container').style;
containerStyle.width = '';
containerStyle.height = '';
containerStyle.position = '';
containerStyle.left = '';
containerStyle.top = '';
containerStyle.zIndex = '';
var currentChart = null,
charts = Highcharts.charts,
templateCharts = [];
// Destroy all charts, except template charts
for (var i = 0, ie = charts.length; i < ie; ++i) {
currentChart = charts[i];
if (!currentChart) {
continue;
}
if (currentChart.template) {
templateCharts.push(currentChart);
currentChart.renderer.box.isTemplate = true;
} else if (currentChart.destroy && currentChart.renderer) {
currentChart.destroy();
}
}
Highcharts.charts.length = 0;
Array.prototype.push.apply(Highcharts.charts, templateCharts);
// Renderer samples, no chart instance existed
var svgs = document.getElementsByTagName('svg'),
i = svgs.length;
while (i--) {
if (!svgs[i].isTemplate) {
svgs[i].parentNode.removeChild(svgs[i]);
}
}
// Unwrap/reset wrapped functions
while (wrappedFunctions.length) {
//const [ ob, prop, fn ] = wrappedFunctions.pop();
var args = wrappedFunctions.pop(),
ob = args[0],
prop = args[1],
fn = args[2];
ob[prop] = fn;
}
Highcharts.wrap = origWrap;
// Unbind events and reset addEvent
while (addedEvents.length) {
addedEvents.pop()();
}
Highcharts.addEvent = origAddEvent;
// Reset defaultOptions and callbacks if those are mutated. In
// karma-konf, the scriptBody is inspected to see if these expensive
// operations are necessary. Visual tests only.
if (test.test.resets && test.test.resets.forEach) {
test.test.resets.forEach(function (key) {
var fn = {
callbacks: function () {
Highcharts.Chart.prototype.callbacks =
Highcharts.callbacksRaw.slice(0);
},
defaultOptions: function () {
resetDefaultOptions(test.test.testName);
}
};
fn[key]();
});
}
}
});
}
/*
* Display the tooltip so it gets part of the comparison
*/
Highcharts.prepareShot = function (chart) {
if (
chart &&
chart.series &&
chart.series[0]
) {
var points = chart.series[0].nodes || // Network graphs, sankey etc
chart.series[0].points;
if (points) {
for (var i = 0; i < points.length; i++) {
if (
points[i] &&
!points[i].isNull &&
!( // Map point with no extent, like Aruba
points[i].shapeArgs &&
points[i].shapeArgs.d &&
points[i].shapeArgs.d.length === 0
) &&
typeof points[i].onMouseOver === 'function'
) {
points[i].onMouseOver();
break;
}
}
}
}
};
/**
* Basic pretty-print SVG, each tag on a new line.
* @param {String} svg The SVG
* @return {String} Pretty SVG
*/
function prettyXML(svg) {
svg = svg
.replace(/>/g, '>\n')
// Don't introduce newlines inside tspans or links, it will make the text
// render differently
.replace(/<tspan([^>]*)>\n/g, '<tspan$1>')
.replace(/<\/tspan>\n/g, '</tspan>')
.replace(/<a([^>]*)>\n/g, '<a$1>')
.replace(/<\/a>\n/g, '</a>');
return svg;
}
/**
* Get the SVG of a chart, or the first SVG in the page
* @param {Object} chart The chart
* @return {String} The SVG
*/
function getSVG(chart) {
var svg;
if (chart) {
var container = chart.container;
Highcharts.prepareShot(chart);
svg = container.querySelector('svg')
.outerHTML
.replace(
/<svg /,
'<svg xmlns:xlink="http://www.w3.org/1999/xlink" '
);
if (chart.styledMode) {
svg = svg.replace(
'</style>',
'* { fill: rgba(0, 0, 0, 0.1); stroke: black; stroke-width: 1px; } '
+ 'text, tspan { fill: blue; stroke: none; } </style>'
);
}
// Renderer samples
} else {
if (document.getElementsByTagName('svg').length) {
svg = document.getElementsByTagName('svg')[0].outerHTML;
}
}
return prettyXML(svg);
}
/**
* Compares the image data of two canvases
* @param {Array} data1 Pixel data for image1.
* @param {Array} data2 Pixel data for image2.
* @return {Number} The amount of different pixels, where 0 is identical
*/
function compare(data1, data2) { // eslint-disable-line no-unused-vars
var i = data1.length,
diff = 0,
pixels = [],
pixel;
// loops over all reds, greens, blues and alphas
while (i--) {
pixel = Math.floor(i / 4);
if (Math.abs(data1[i] - data2[i]) !== 0 && !pixels[pixel]) {
pixels[pixel] = true;
diff++;
}
}
return diff;
}
/**
* Vanilla request for fetching an url using GET.
* @param {String} url to fetch
* @param {Function} callback to call when done.
*/
function xhrLoad(url, callback) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
callback(xhr);
}
};
xhr.open('GET', url, true);
xhr.send();
}
function loadReferenceSVG(path) {
return new Promise(function (resolve, reject) {
var remotelocation = __karma__.config.cliArgs && __karma__.config.cliArgs.remotelocation;
// Handle reference, load SVG from bucket or file
var url = 'base/samples/' + path + '/reference.svg';
if (remotelocation) {
url = 'http://' + remotelocation + '.s3.eu-central-1.amazonaws.com/visualtests/reference/latest/' + path + '/reference.svg';
}
xhrLoad(url, function onXHRDone(xhr) {
if (xhr.status === 200) {
var svg = xhr.responseText;
resolve(svg);
} else {
var errMsg = 'Unable to load svg for test ' + path + ' found. Skipping comparison.'
+ ' Status returned is ' + xhr.status + ' ' + xhr.statusText + '.';
reject(new Error(errMsg));
}
});
})
}
/**
* Creates a SVG snapshot of the chart and sends to karma for storage.
*
* @param {string} svg The chart svg
* @param {string} path of the sample/test
*/
function saveSVGSnapshot(svg, path) {
if (svg) {
__karma__.info({
filename: './samples/' + path,
data: svg
});
}
}
function svgToPixels(svg, canvas) {
var DOMURL = (window.URL || window.webkitURL || window);
var ctx = canvas.getContext && canvas.getContext('2d');
// Invalidate images, loading external images will throw an error
// svg = svg.replace(/xlink:href/g, 'data-href');
var blob = new Blob([svg], { type: 'image/svg+xml' });
var img = new Image(CANVAS_WIDTH, CANVAS_HEIGHT);
img.src = DOMURL.createObjectURL(blob);
return new Promise(function (resolve, reject) {
img.onload = function () {
ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
ctx.drawImage(img, 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
resolve(ctx.getImageData(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT).data);
DOMURL.revokeObjectURL(img.src);
};
img.onerror = function () {
DOMURL.revokeObjectURL(img.src);
reject(new Error('Error loading SVG on canvas.'));
};
});
}
function createCanvas(id) {
var canvas = document.createElement('canvas');
canvas.setAttribute('id', id);
canvas.setAttribute('width', CANVAS_WIDTH);
canvas.setAttribute('height', CANVAS_HEIGHT);
return canvas;
}
/**
* Get a PNG image or image data from the chart SVG
* and compares it with a reference svg already stored on the system.
*
* @param {Object} chart The chart instance
* @param {String} path The sample path
* @return {String} The image data
*/
function compareToReference(chart, path) { // eslint-disable-line no-unused-vars
return new Promise(function (resolve, reject) {
var candidateSVG = getSVG(chart);
if (!candidateSVG || !path) {
reject(new Error('No candidate SVG found for path: ' + path));
}
var referenceCanvas = createCanvas('reference');
var candidateCanvas = createCanvas('candidate');
var candidatePixels = svgToPixels(candidateSVG, candidateCanvas);
loadReferenceSVG(path)
.then(function (referenceSVG) {
return Promise.all([
svgToPixels(referenceSVG, referenceCanvas),
candidatePixels
]);
})
.then(function (pixelsInFile) {
var referencePixels = pixelsInFile[0];
var candidatePixels = pixelsInFile[1];
var diff = compare(referencePixels, candidatePixels);
if (diff !== 0) {
__karma__.info({
filename: './samples/' + path + '/diff.gif',
canvasWidth: CANVAS_WIDTH,
canvasHeight: CANVAS_HEIGHT,
frames: [
referencePixels,
candidatePixels
]
});
saveSVGSnapshot(candidateSVG, path + '/candidate.svg');
}
resolve(diff);
})
['catch'](function (error) { // to avoid IE8 failure
console.log(error && error.message);
resolve(error && error.message); // skip and continue processing
});
});
}
// De-randomize Math.random in tests
(function () {
var randomValues = [0.14102989272214472, 0.0351817375048995,
0.10094573209062219, 0.35990892769768834, 0.7690574480220675,
0.16634021210484207, 0.3944594960194081, 0.7656398438848555,
0.27706647920422256, 0.5681763959582895, 0.513730650767684,
0.26344996923580766, 0.09001278411597013, 0.2977627406362444,
0.6982127586379647, 0.9593012358527631, 0.8456065070349723,
0.26248381356708705, 0.12872424302622676, 0.25530692492611706,
0.9969052199739963, 0.09259856841526926, 0.9022860133554786,
0.3393681487068534, 0.41671016393229365, 0.10582929337397218,
0.1322793234139681, 0.595869708340615, 0.050670077092945576,
0.8613549116998911, 0.17356411134824157, 0.16447093593887985,
0.44514468451961875, 0.15736589767038822, 0.8677479331381619,
0.30932203005068004, 0.6120233973488212, 0.001859797164797783,
0.7689258102327585, 0.7421043077483773, 0.7548440918326378,
0.9667320610024035, 0.13654314493760467, 0.6277681242208928,
0.002858637133613229, 0.6877673089038581, 0.44036358245648444,
0.3101970909629017, 0.013212101766839623, 0.7115063068922609,
0.2931885647121817, 0.5031651991885155, 0.8921459852717817,
0.547999506117776, 0.010382920736446977, 0.9862914837431163,
0.9629317701328546, 0.07685352209955454, 0.2859949553385377,
0.5578324059024453, 0.7765828191768378, 0.1696563793811947,
0.34366130153648555, 0.11959927808493376, 0.8898638435639441,
0.8963573810178787, 0.332408863119781, 0.27137733018025756,
0.3066735703032464, 0.2789501305669546, 0.4567076754756272,
0.09539463231340051, 0.9158625246491283, 0.2145260546822101,
0.8913846455980092, 0.22340057184919715, 0.09033847553655505,
0.49042539740912616, 0.4070818084292114, 0.5827512110117823,
0.1993762720376253, 0.9264022477436811, 0.3290765874553472,
0.07792594563215971, 0.7663758248090744, 0.4329648329876363,
0.10257583996281028, 0.8170149670913815, 0.41387700103223324,
0.7504217880778015, 0.08603733032941818, 0.17256441875360906,
0.4064991301856935, 0.829071992309764, 0.6997416105587035,
0.2686419754754752, 0.36025605257600546, 0.6014082923065871,
0.9787689209915698, 0.016065671807155013];
Math.randomCursor = 0;
Math.random = function () {
var ret = randomValues[
Math.randomCursor % randomValues.length
];
Math.randomCursor++;
return ret;
};
}());