describe('Core helper tests', function() { var helpers; beforeAll(function() { helpers = window.Chart.helpers; }); it('should iterate over an array and pass the extra data to that function', function() { var testData = [0, 9, 'abc']; var scope = {}; // fake out the scope and ensure that 'this' is the correct thing helpers.each(testData, function(item, index) { expect(item).not.toBe(undefined); expect(index).not.toBe(undefined); expect(testData[index]).toBe(item); expect(this).toBe(scope); }, scope); // Reverse iteration var iterated = []; helpers.each(testData, function(item, index) { expect(item).not.toBe(undefined); expect(index).not.toBe(undefined); expect(testData[index]).toBe(item); expect(this).toBe(scope); iterated.push(item); }, scope, true); expect(iterated.slice().reverse()).toEqual(testData); }); it('should iterate over properties in an object', function() { var testData = { myProp1: 'abc', myProp2: 276, myProp3: ['a', 'b'] }; helpers.each(testData, function(value, key) { if (key === 'myProp1') { expect(value).toBe('abc'); } else if (key === 'myProp2') { expect(value).toBe(276); } else if (key === 'myProp3') { expect(value).toEqual(['a', 'b']); } else { expect(false).toBe(true); } }); }); it('should not error when iterating over a null object', function() { expect(function() { helpers.each(undefined); }).not.toThrow(); }); it('should clone an object', function() { var testData = { myProp1: 'abc', myProp2: ['a', 'b'], myProp3: { myProp4: 5, myProp5: [1, 2] } }; var clone = helpers.clone(testData); expect(clone).toEqual(testData); expect(clone).not.toBe(testData); expect(clone.myProp2).not.toBe(testData.myProp2); expect(clone.myProp3).not.toBe(testData.myProp3); expect(clone.myProp3.myProp5).not.toBe(testData.myProp3.myProp5); }); it('should extend an object', function() { var original = { myProp1: 'abc', myProp2: 56 }; var extension = { myProp3: [2, 5, 6], myProp2: 0 }; helpers.extend(original, extension); expect(original).toEqual({ myProp1: 'abc', myProp2: 0, myProp3: [2, 5, 6], }); }); it('should merge a normal config without scales', function() { var baseConfig = { valueProp: 5, arrayProp: [1, 2, 3, 4, 5, 6], objectProp: { prop1: 'abc', prop2: 56 } }; var toMerge = { valueProp2: null, arrayProp: ['a', 'c'], objectProp: { prop1: 'c', prop3: 'prop3' } }; var merged = helpers.configMerge(baseConfig, toMerge); expect(merged).toEqual({ valueProp: 5, valueProp2: null, arrayProp: ['a', 'c'], objectProp: { prop1: 'c', prop2: 56, prop3: 'prop3' } }); }); it('should merge scale configs', function() { var baseConfig = { scales: { prop1: { abc: 123, def: '456' }, prop2: 777, yAxes: [{ type: 'linear', }, { type: 'log' }] } }; var toMerge = { scales: { prop1: { def: 'bbb', ghi: 78 }, prop2: null, yAxes: [{ type: 'linear', axisProp: 456 }, { // pulls in linear default config since axis type changes type: 'linear', position: 'right' }, { // Pulls in linear default config since axis not in base type: 'linear' }] } }; var merged = helpers.configMerge(baseConfig, toMerge); expect(merged).toEqual({ scales: { prop1: { abc: 123, def: 'bbb', ghi: 78 }, prop2: null, yAxes: [{ type: 'linear', axisProp: 456 }, { display: true, gridLines: { color: 'rgba(0, 0, 0, 0.1)', drawBorder: true, drawOnChartArea: true, drawTicks: true, // draw ticks extending towards the label tickMarkLength: 10, lineWidth: 1, offsetGridLines: false, display: true, zeroLineColor: 'rgba(0,0,0,0.25)', zeroLineWidth: 1, borderDash: [], borderDashOffset: 0.0 }, position: 'right', scaleLabel: { labelString: '', display: false, }, ticks: { beginAtZero: false, minRotation: 0, maxRotation: 50, mirror: false, padding: 0, reverse: false, display: true, callback: merged.scales.yAxes[1].ticks.callback, // make it nicer, then check explicitly below autoSkip: true, autoSkipPadding: 0, labelOffset: 0, }, type: 'linear' }, { display: true, gridLines: { color: 'rgba(0, 0, 0, 0.1)', drawBorder: true, drawOnChartArea: true, drawTicks: true, // draw ticks extending towards the label, tickMarkLength: 10, lineWidth: 1, offsetGridLines: false, display: true, zeroLineColor: 'rgba(0,0,0,0.25)', zeroLineWidth: 1, borderDash: [], borderDashOffset: 0.0 }, position: 'left', scaleLabel: { labelString: '', display: false, }, ticks: { beginAtZero: false, minRotation: 0, maxRotation: 50, mirror: false, padding: 0, reverse: false, display: true, callback: merged.scales.yAxes[2].ticks.callback, // make it nicer, then check explicitly below autoSkip: true, autoSkipPadding: 0, labelOffset: 0, }, type: 'linear' }] } }); // Are these actually functions expect(merged.scales.yAxes[1].ticks.callback).toEqual(jasmine.any(Function)); expect(merged.scales.yAxes[2].ticks.callback).toEqual(jasmine.any(Function)); }); it('should get value or default', function() { expect(helpers.getValueAtIndexOrDefault(98, 0, 56)).toBe(98); expect(helpers.getValueAtIndexOrDefault(0, 0, 56)).toBe(0); expect(helpers.getValueAtIndexOrDefault(undefined, undefined, 56)).toBe(56); expect(helpers.getValueAtIndexOrDefault([1, 2, 3], 1, 100)).toBe(2); expect(helpers.getValueAtIndexOrDefault([1, 2, 3], 3, 100)).toBe(100); }); it('should filter an array', function() { var data = [-10, 0, 6, 0, 7]; var callback = function(item) { return item > 2; }; expect(helpers.where(data, callback)).toEqual([6, 7]); expect(helpers.findNextWhere(data, callback)).toEqual(6); expect(helpers.findNextWhere(data, callback, 2)).toBe(7); expect(helpers.findNextWhere(data, callback, 4)).toBe(undefined); expect(helpers.findPreviousWhere(data, callback)).toBe(7); expect(helpers.findPreviousWhere(data, callback, 3)).toBe(6); expect(helpers.findPreviousWhere(data, callback, 0)).toBe(undefined); }); it('should get the correct sign', function() { expect(helpers.sign(0)).toBe(0); expect(helpers.sign(10)).toBe(1); expect(helpers.sign(-5)).toBe(-1); }); it('should do a log10 operation', function() { expect(helpers.log10(0)).toBe(-Infinity); expect(helpers.log10(1)).toBe(0); expect(helpers.log10(1000)).toBeCloseTo(3, 1e-9); }); it('should correctly determine if two numbers are essentially equal', function() { expect(helpers.almostEquals(0, Number.EPSILON, 2 * Number.EPSILON)).toBe(true); expect(helpers.almostEquals(1, 1.1, 0.0001)).toBe(false); expect(helpers.almostEquals(1e30, 1e30 + Number.EPSILON, 0)).toBe(false); expect(helpers.almostEquals(1e30, 1e30 + Number.EPSILON, 2 * Number.EPSILON)).toBe(true); }); it('should generate integer ids', function() { var uid = helpers.uid(); expect(uid).toEqual(jasmine.any(Number)); expect(helpers.uid()).toBe(uid + 1); expect(helpers.uid()).toBe(uid + 2); expect(helpers.uid()).toBe(uid + 3); }); it('should detect a number', function() { expect(helpers.isNumber(123)).toBe(true); expect(helpers.isNumber('123')).toBe(true); expect(helpers.isNumber(null)).toBe(false); expect(helpers.isNumber(NaN)).toBe(false); expect(helpers.isNumber(undefined)).toBe(false); expect(helpers.isNumber('cbc')).toBe(false); }); it('should convert between radians and degrees', function() { expect(helpers.toRadians(180)).toBe(Math.PI); expect(helpers.toRadians(90)).toBe(0.5 * Math.PI); expect(helpers.toDegrees(Math.PI)).toBe(180); expect(helpers.toDegrees(Math.PI * 3 / 2)).toBe(270); }); it('should get an angle from a point', function() { var center = { x: 0, y: 0 }; expect(helpers.getAngleFromPoint(center, { x: 0, y: 10 })).toEqual({ angle: Math.PI / 2, distance: 10, }); expect(helpers.getAngleFromPoint(center, { x: Math.sqrt(2), y: Math.sqrt(2) })).toEqual({ angle: Math.PI / 4, distance: 2 }); expect(helpers.getAngleFromPoint(center, { x: -1.0 * Math.sqrt(2), y: -1.0 * Math.sqrt(2) })).toEqual({ angle: Math.PI * 1.25, distance: 2 }); }); it('should spline curves', function() { expect(helpers.splineCurve({ x: 0, y: 0 }, { x: 1, y: 1 }, { x: 2, y: 0 }, 0)).toEqual({ previous: { x: 1, y: 1, }, next: { x: 1, y: 1, } }); expect(helpers.splineCurve({ x: 0, y: 0 }, { x: 1, y: 1 }, { x: 2, y: 0 }, 1)).toEqual({ previous: { x: 0, y: 1, }, next: { x: 2, y: 1, } }); }); it('should spline curves with monotone cubic interpolation', function() { var dataPoints = [ {_model: {x: 0, y: 0, skip: false}}, {_model: {x: 3, y: 6, skip: false}}, {_model: {x: 9, y: 6, skip: false}}, {_model: {x: 12, y: 60, skip: false}}, {_model: {x: 15, y: 60, skip: false}}, {_model: {x: 18, y: 120, skip: false}}, {_model: {x: null, y: null, skip: true}}, {_model: {x: 21, y: 180, skip: false}}, {_model: {x: 24, y: 120, skip: false}}, {_model: {x: 27, y: 125, skip: false}}, {_model: {x: 30, y: 105, skip: false}}, {_model: {x: 33, y: 110, skip: false}}, {_model: {x: 33, y: 110, skip: false}}, {_model: {x: 36, y: 170, skip: false}} ]; helpers.splineCurveMonotone(dataPoints); expect(dataPoints).toEqual([{ _model: { x: 0, y: 0, skip: false, controlPointNextX: 1, controlPointNextY: 2 } }, { _model: { x: 3, y: 6, skip: false, controlPointPreviousX: 2, controlPointPreviousY: 6, controlPointNextX: 5, controlPointNextY: 6 } }, { _model: { x: 9, y: 6, skip: false, controlPointPreviousX: 7, controlPointPreviousY: 6, controlPointNextX: 10, controlPointNextY: 6 } }, { _model: { x: 12, y: 60, skip: false, controlPointPreviousX: 11, controlPointPreviousY: 60, controlPointNextX: 13, controlPointNextY: 60 } }, { _model: { x: 15, y: 60, skip: false, controlPointPreviousX: 14, controlPointPreviousY: 60, controlPointNextX: 16, controlPointNextY: 60 } }, { _model: { x: 18, y: 120, skip: false, controlPointPreviousX: 17, controlPointPreviousY: 100 } }, { _model: { x: null, y: null, skip: true } }, { _model: { x: 21, y: 180, skip: false, controlPointNextX: 22, controlPointNextY: 160 } }, { _model: { x: 24, y: 120, skip: false, controlPointPreviousX: 23, controlPointPreviousY: 120, controlPointNextX: 25, controlPointNextY: 120 } }, { _model: { x: 27, y: 125, skip: false, controlPointPreviousX: 26, controlPointPreviousY: 125, controlPointNextX: 28, controlPointNextY: 125 } }, { _model: { x: 30, y: 105, skip: false, controlPointPreviousX: 29, controlPointPreviousY: 105, controlPointNextX: 31, controlPointNextY: 105 } }, { _model: { x: 33, y: 110, skip: false, controlPointPreviousX: 32, controlPointPreviousY: 110, controlPointNextX: 33, controlPointNextY: 110 } }, { _model: { x: 33, y: 110, skip: false, controlPointPreviousX: 33, controlPointPreviousY: 110, controlPointNextX: 34, controlPointNextY: 110 } }, { _model: { x: 36, y: 170, skip: false, controlPointPreviousX: 35, controlPointPreviousY: 150 } }]); }); it('should get the next or previous item in an array', function() { var testData = [0, 1, 2]; expect(helpers.nextItem(testData, 0, false)).toEqual(1); expect(helpers.nextItem(testData, 2, false)).toEqual(2); expect(helpers.nextItem(testData, 2, true)).toEqual(0); expect(helpers.nextItem(testData, 1, true)).toEqual(2); expect(helpers.nextItem(testData, -1, false)).toEqual(0); expect(helpers.previousItem(testData, 0, false)).toEqual(0); expect(helpers.previousItem(testData, 0, true)).toEqual(2); expect(helpers.previousItem(testData, 2, false)).toEqual(1); expect(helpers.previousItem(testData, 1, true)).toEqual(0); }); it('should clear a canvas', function() { var context = window.createMockContext(); helpers.clear({ width: 100, height: 150, ctx: context }); expect(context.getCalls()).toEqual([{ name: 'clearRect', args: [0, 0, 100, 150] }]); }); it('should return the width of the longest text in an Array and 2D Array', function() { var context = window.createMockContext(); var font = "normal 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"; var arrayOfThings1D = ['FooBar', 'Bar']; var arrayOfThings2D = [['FooBar_1', 'Bar_2'], 'Foo_1']; // Regardless 'FooBar' is the longest label it should return (characters * 10) expect(helpers.longestText(context, font, arrayOfThings1D, {})).toEqual(60); expect(helpers.longestText(context, font, arrayOfThings2D, {})).toEqual(80); // We check to make sure we made the right calls to the canvas. expect(context.getCalls()).toEqual([{ name: 'measureText', args: ['FooBar'] }, { name: 'measureText', args: ['Bar'] }, { name: 'measureText', args: ['FooBar_1'] }, { name: 'measureText', args: ['Bar_2'] }, { name: 'measureText', args: ['Foo_1'] }]); }); it('compare text with current longest and update', function() { var context = window.createMockContext(); var data = {}; var gc = []; var longest = 70; expect(helpers.measureText(context, data, gc, longest, 'foobar')).toEqual(70); expect(helpers.measureText(context, data, gc, longest, 'foobar_')).toEqual(70); expect(helpers.measureText(context, data, gc, longest, 'foobar_1')).toEqual(80); // We check to make sure we made the right calls to the canvas. expect(context.getCalls()).toEqual([{ name: 'measureText', args: ['foobar'] }, { name: 'measureText', args: ['foobar_'] }, { name: 'measureText', args: ['foobar_1'] }]); }); it('count look at all the labels and return maximum number of lines', function() { window.createMockContext(); var arrayOfThings1 = ['Foo', 'Bar']; var arrayOfThings2 = [['Foo', 'Bar'], 'Foo']; var arrayOfThings3 = [['Foo', 'Bar', 'Boo'], ['Foo', 'Bar'], 'Foo']; expect(helpers.numberOfLabelLines(arrayOfThings1)).toEqual(1); expect(helpers.numberOfLabelLines(arrayOfThings2)).toEqual(2); expect(helpers.numberOfLabelLines(arrayOfThings3)).toEqual(3); }); it('should draw a rounded rectangle', function() { var context = window.createMockContext(); helpers.drawRoundedRectangle(context, 10, 20, 30, 40, 5); expect(context.getCalls()).toEqual([{ name: 'beginPath', args: [] }, { name: 'moveTo', args: [15, 20] }, { name: 'lineTo', args: [35, 20] }, { name: 'quadraticCurveTo', args: [40, 20, 40, 25] }, { name: 'lineTo', args: [40, 55] }, { name: 'quadraticCurveTo', args: [40, 60, 35, 60] }, { name: 'lineTo', args: [15, 60] }, { name: 'quadraticCurveTo', args: [10, 60, 10, 55] }, { name: 'lineTo', args: [10, 25] }, { name: 'quadraticCurveTo', args: [10, 20, 15, 20] }, { name: 'closePath', args: [] }]); }); it ('should get the maximum width and height for a node', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); div.style.width = '200px'; div.style.height = '300px'; document.body.appendChild(div); // Create the div we want to get the max size for var innerDiv = document.createElement('div'); div.appendChild(innerDiv); expect(helpers.getMaximumWidth(innerDiv)).toBe(200); expect(helpers.getMaximumHeight(innerDiv)).toBe(300); document.body.removeChild(div); }); it ('should get the maximum width of a node that has a max-width style', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); div.style.width = '200px'; div.style.height = '300px'; document.body.appendChild(div); // Create the div we want to get the max size for and set a max-width style var innerDiv = document.createElement('div'); innerDiv.style.maxWidth = '150px'; div.appendChild(innerDiv); expect(helpers.getMaximumWidth(innerDiv)).toBe(150); document.body.removeChild(div); }); it ('should get the maximum height of a node that has a max-height style', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); div.style.width = '200px'; div.style.height = '300px'; document.body.appendChild(div); // Create the div we want to get the max size for and set a max-height style var innerDiv = document.createElement('div'); innerDiv.style.maxHeight = '150px'; div.appendChild(innerDiv); expect(helpers.getMaximumHeight(innerDiv)).toBe(150); document.body.removeChild(div); }); it ('should get the maximum width of a node when the parent has a max-width style', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); div.style.width = '200px'; div.style.height = '300px'; document.body.appendChild(div); // Create an inner wrapper around our div we want to size and give that a max-width style var parentDiv = document.createElement('div'); parentDiv.style.maxWidth = '150px'; div.appendChild(parentDiv); // Create the div we want to get the max size for var innerDiv = document.createElement('div'); parentDiv.appendChild(innerDiv); expect(helpers.getMaximumWidth(innerDiv)).toBe(150); document.body.removeChild(div); }); it ('should get the maximum height of a node when the parent has a max-height style', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); div.style.width = '200px'; div.style.height = '300px'; document.body.appendChild(div); // Create an inner wrapper around our div we want to size and give that a max-height style var parentDiv = document.createElement('div'); parentDiv.style.maxHeight = '150px'; div.appendChild(parentDiv); // Create the div we want to get the max size for var innerDiv = document.createElement('div'); innerDiv.style.height = '300px'; // make it large parentDiv.appendChild(innerDiv); expect(helpers.getMaximumHeight(innerDiv)).toBe(150); document.body.removeChild(div); }); it ('should get the maximum width of a node that has a percentage max-width style', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); div.style.width = '200px'; div.style.height = '300px'; document.body.appendChild(div); // Create the div we want to get the max size for and set a max-width style var innerDiv = document.createElement('div'); innerDiv.style.maxWidth = '50%'; div.appendChild(innerDiv); expect(helpers.getMaximumWidth(innerDiv)).toBe(100); document.body.removeChild(div); }); it ('should get the maximum height of a node that has a percentage max-height style', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); div.style.width = '200px'; div.style.height = '300px'; document.body.appendChild(div); // Create the div we want to get the max size for and set a max-height style var innerDiv = document.createElement('div'); innerDiv.style.maxHeight = '50%'; div.appendChild(innerDiv); expect(helpers.getMaximumHeight(innerDiv)).toBe(150); document.body.removeChild(div); }); it ('should get the maximum width of a node when the parent has a percentage max-width style', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); div.style.width = '200px'; div.style.height = '300px'; document.body.appendChild(div); // Create an inner wrapper around our div we want to size and give that a max-width style var parentDiv = document.createElement('div'); parentDiv.style.maxWidth = '50%'; div.appendChild(parentDiv); // Create the div we want to get the max size for var innerDiv = document.createElement('div'); parentDiv.appendChild(innerDiv); expect(helpers.getMaximumWidth(innerDiv)).toBe(100); document.body.removeChild(div); }); it ('should get the maximum height of a node when the parent has a percentage max-height style', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); div.style.width = '200px'; div.style.height = '300px'; document.body.appendChild(div); // Create an inner wrapper around our div we want to size and give that a max-height style var parentDiv = document.createElement('div'); parentDiv.style.maxHeight = '50%'; div.appendChild(parentDiv); var innerDiv = document.createElement('div'); innerDiv.style.height = '300px'; // make it large parentDiv.appendChild(innerDiv); expect(helpers.getMaximumHeight(innerDiv)).toBe(150); document.body.removeChild(div); }); describe('Color helper', function() { function isColorInstance(obj) { return typeof obj === 'object' && obj.hasOwnProperty('values') && obj.values.hasOwnProperty('rgb'); } it('should return a color when called with a color', function() { expect(isColorInstance(helpers.color('rgb(1, 2, 3)'))).toBe(true); }); it('should return a color when called with a CanvasGradient instance', function() { var context = document.createElement('canvas').getContext('2d'); var gradient = context.createLinearGradient(0, 1, 2, 3); expect(isColorInstance(helpers.color(gradient))).toBe(true); }); }); describe('Background hover color helper', function() { it('should return a CanvasPattern when called with a CanvasPattern', function(done) { var dots = new Image(); dots.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAMAAAAolt3jAAAAD1BMVEUAAAD///////////////+PQt5oAAAABXRSTlMAHlFhZsfk/BEAAAAqSURBVHgBY2BgZGJmYmSAAUYWEIDzmcBcJhiXGcxlRpPFrhdmMiqgvX0AcGIBEUAo6UAAAAAASUVORK5CYII='; dots.onload = function() { var chartContext = document.createElement('canvas').getContext('2d'); var patternCanvas = document.createElement('canvas'); var patternContext = patternCanvas.getContext('2d'); var pattern = patternContext.createPattern(dots, 'repeat'); patternContext.fillStyle = pattern; var backgroundColor = helpers.getHoverColor(chartContext.createPattern(patternCanvas, 'repeat')); expect(backgroundColor instanceof CanvasPattern).toBe(true); done(); }; }); it('should return a modified version of color when called with a color', function() { var originalColorRGB = 'rgb(70, 191, 189)'; expect(helpers.getHoverColor('#46BFBD')).not.toEqual(originalColorRGB); }); }); });