Merge pull request #1425 from etimberg/feature/unit-test

Unit tests for elements
This commit is contained in:
Evert Timberg 2015-08-29 14:19:01 -04:00
commit accb5ed654
11 changed files with 1690 additions and 142 deletions

View File

@ -57,12 +57,6 @@
});
return base;
},
merge = helpers.merge = function(base, master) {
//Merge properties in left object over to a shallow clone of object right.
var args = Array.prototype.slice.call(arguments, 0);
args.unshift({});
return extend.apply(null, args);
},
// Need a special merge function to chart configs since they are now grouped
configMerge = helpers.configMerge = function(_base) {
var base = clone(_base);
@ -84,7 +78,13 @@
helpers.each(value, function(valueObj, index) {
if (index < baseArray.length) {
baseArray[index] = helpers.configMerge(baseArray[index], valueObj);
if (typeof baseArray[index] == 'object' && baseArray[index] !== null && typeof valueObj == 'object' && valueObj !== null) {
// Two objects are coming together. Do a merge of them.
baseArray[index] = helpers.configMerge(baseArray[index], valueObj);
} else {
// Just overwrite in this case since there is nothing to merge
baseArray[index] = valueObj;
}
} else {
baseArray.push(valueObj); // nothing to merge
}
@ -143,12 +143,12 @@
return base;
},
getValueAtIndexOrDefault = helpers.getValueAtIndexOrDefault = function(value, index, defaultValue) {
if (!value) {
if (value === undefined || value === null) {
return defaultValue;
}
if (helpers.isArray(value) && index < value.length) {
return value[index];
if (helpers.isArray(value)) {
return index < value.length ? value[index] : defaultValue;
}
return value;
@ -176,7 +176,7 @@
},
findNextWhere = helpers.findNextWhere = function(arrayToSearch, filterCallback, startIndex) {
// Default to start of the array
if (!startIndex) {
if (startIndex === undefined || startIndex === null) {
startIndex = -1;
}
for (var i = startIndex + 1; i < arrayToSearch.length; i++) {
@ -188,7 +188,7 @@
},
findPreviousWhere = helpers.findPreviousWhere = function(arrayToSearch, filterCallback, startIndex) {
// Default to end of the array
if (!startIndex) {
if (startIndex === undefined || startIndex === null) {
startIndex = arrayToSearch.length;
}
for (var i = startIndex - 1; i >= 0; i--) {
@ -259,18 +259,6 @@
return Math.log(x) / Math.LN10;
}
},
cap = helpers.cap = function(valueToCap, maxValue, minValue) {
if (isNumber(maxValue)) {
if (valueToCap > maxValue) {
return maxValue;
}
} else if (isNumber(minValue)) {
if (valueToCap < minValue) {
return minValue;
}
}
return valueToCap;
},
getDecimalPlaces = helpers.getDecimalPlaces = function(num) {
if (num % 1 !== 0 && isNumber(num)) {
var s = num.toString();
@ -335,94 +323,16 @@
},
nextItem = helpers.nextItem = function(collection, index, loop) {
if (loop) {
return collection[index + 1] || collection[0];
return index >= collection.length - 1 ? collection[0] : collection[index + 1];
}
return collection[index + 1] || collection[collection.length - 1];
return index >= collection.length - 1 ? collection[collection.length - 1] : collection[index + 1];
},
previousItem = helpers.previousItem = function(collection, index, loop) {
if (loop) {
return collection[index - 1] || collection[collection.length - 1];
return index <= 0 ? collection[collection.length - 1] : collection[index - 1];
}
return collection[index - 1] || collection[0];
},
calculateOrderOfMagnitude = helpers.calculateOrderOfMagnitude = function(val) {
return Math.floor(Math.log(val) / Math.LN10);
},
calculateScaleRange = helpers.calculateScaleRange = function(valuesArray, drawingSize, textSize, startFromZero, integersOnly) {
//Set a minimum step of two - a point at the top of the graph, and a point at the base
var minSteps = 2,
maxSteps = Math.floor(drawingSize / (textSize * 1.5)),
skipFitting = (minSteps >= maxSteps);
var maxValue = max(valuesArray),
minValue = min(valuesArray);
// We need some degree of seperation here to calculate the scales if all the values are the same
// Adding/minusing 0.5 will give us a range of 1.
if (maxValue === minValue) {
maxValue += 0.5;
// So we don't end up with a graph with a negative start value if we've said always start from zero
if (minValue >= 0.5 && !startFromZero) {
minValue -= 0.5;
} else {
// Make up a whole number above the values
maxValue += 0.5;
}
}
var valueRange = Math.abs(maxValue - minValue),
rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange),
graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude),
graphMin = (startFromZero) ? 0 : Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude),
graphRange = graphMax - graphMin,
stepValue = Math.pow(10, rangeOrderOfMagnitude),
numberOfSteps = Math.round(graphRange / stepValue);
//If we have more space on the graph we'll use it to give more definition to the data
while ((numberOfSteps > maxSteps || (numberOfSteps * 2) < maxSteps) && !skipFitting) {
if (numberOfSteps > maxSteps) {
stepValue *= 2;
numberOfSteps = Math.round(graphRange / stepValue);
// Don't ever deal with a decimal number of steps - cancel fitting and just use the minimum number of steps.
if (numberOfSteps % 1 !== 0) {
skipFitting = true;
}
}
//We can fit in double the amount of scale points on the scale
else {
//If user has declared ints only, and the step value isn't a decimal
if (integersOnly && rangeOrderOfMagnitude >= 0) {
//If the user has said integers only, we need to check that making the scale more granular wouldn't make it a float
if (stepValue / 2 % 1 === 0) {
stepValue /= 2;
numberOfSteps = Math.round(graphRange / stepValue);
}
//If it would make it a float break out of the loop
else {
break;
}
}
//If the scale doesn't have to be an int, make the scale more granular anyway.
else {
stepValue /= 2;
numberOfSteps = Math.round(graphRange / stepValue);
}
}
}
if (skipFitting) {
numberOfSteps = minSteps;
stepValue = graphRange / numberOfSteps;
}
return {
steps: numberOfSteps,
stepValue: stepValue,
min: graphMin,
max: graphMin + (numberOfSteps * stepValue)
};
return index <= 0 ? collection[0] : collection[index - 1];
},
// Implementation of the nice number algorithm used in determining where axis labels will go
niceNum = helpers.niceNum = function(range, round) {
@ -505,17 +415,6 @@
return tmpl(templateString, valuesObject);
},
/* jshint ignore:end */
generateLabels = helpers.generateLabels = function(templateString, numberOfSteps, graphMin, stepValue) {
var labelsArray = new Array(numberOfSteps);
if (templateString) {
each(labelsArray, function(val, index) {
labelsArray[index] = template(templateString, {
value: (graphMin + (stepValue * (index + 1)))
});
});
}
return labelsArray;
},
//--Animation methods
//Easing functions adapted from Robert Penner's easing equations
//http://www.robertpenner.com/easing/

View File

@ -37,21 +37,24 @@
var vm = this._view;
var pointRelativePosition = helpers.getAngleFromPoint(vm, {
x: chartX,
y: chartY
});
if (vm) {
var pointRelativePosition = helpers.getAngleFromPoint(vm, {
x: chartX,
y: chartY
});
// Put into the range of (-PI/2, 3PI/2]
var startAngle = vm.startAngle < (-0.5 * Math.PI) ? vm.startAngle + (2.0 * Math.PI) : vm.startAngle > (1.5 * Math.PI) ? vm.startAngle - (2.0 * Math.PI) : vm.startAngle;
var endAngle = vm.endAngle < (-0.5 * Math.PI) ? vm.endAngle + (2.0 * Math.PI) : vm.endAngle > (1.5 * Math.PI) ? vm.endAngle - (2.0 * Math.PI) : vm.endAngle
// Put into the range of (-PI/2, 3PI/2]
var startAngle = vm.startAngle < (-0.5 * Math.PI) ? vm.startAngle + (2.0 * Math.PI) : vm.startAngle > (1.5 * Math.PI) ? vm.startAngle - (2.0 * Math.PI) : vm.startAngle;
var endAngle = vm.endAngle < (-0.5 * Math.PI) ? vm.endAngle + (2.0 * Math.PI) : vm.endAngle > (1.5 * Math.PI) ? vm.endAngle - (2.0 * Math.PI) : vm.endAngle
//Check if within the range of the open/close angle
var betweenAngles = (pointRelativePosition.angle >= startAngle && pointRelativePosition.angle <= endAngle),
withinRadius = (pointRelativePosition.distance >= vm.innerRadius && pointRelativePosition.distance <= vm.outerRadius);
//Check if within the range of the open/close angle
var betweenAngles = (pointRelativePosition.angle >= startAngle && pointRelativePosition.angle <= endAngle),
withinRadius = (pointRelativePosition.distance >= vm.innerRadius && pointRelativePosition.distance <= vm.outerRadius);
return (betweenAngles && withinRadius);
//Ensure within the outside of the arc centre, but inside arc outer
return (betweenAngles && withinRadius);
} else {
return false;
}
},
tooltipPosition: function() {
var vm = this._view;

View File

@ -114,7 +114,7 @@
// Now draw the line between all the points with any borders
ctx.lineCap = vm.borderCapStyle || Chart.defaults.global.elements.line.borderCapStyle;
// IE 9 and 10 do not support line dash
if (ctx.setLineDash) {
ctx.setLineDash(vm.borderDash || Chart.defaults.global.elements.line.borderDash);
@ -122,7 +122,7 @@
ctx.lineDashOffset = vm.borderDashOffset || Chart.defaults.global.elements.line.borderDashOffset;
ctx.lineJoin = vm.borderJoinStyle || Chart.defaults.global.elements.line.borderJoinStyle;
ctx.lineWidth = vm.borderWidth || Chart.defaults.global.defaultColor;
ctx.lineWidth = vm.borderWidth || Chart.defaults.global.elements.line.borderWidth;
ctx.strokeStyle = vm.borderColor || Chart.defaults.global.defaultColor;
ctx.beginPath();
@ -184,10 +184,9 @@
}
}
ctx.stroke();
ctx.restore();
},
});
}).call(this);
}).call(this);

View File

@ -32,8 +32,13 @@
Chart.elements.Point = Chart.Element.extend({
inRange: function(mouseX, mouseY) {
var vm = this._view;
var hoverRange = vm.hitRadius + vm.radius;
return ((Math.pow(mouseX - vm.x, 2) + Math.pow(mouseY - vm.y, 2)) < Math.pow(hoverRange, 2));
if (vm) {
var hoverRange = vm.hitRadius + vm.radius;
return ((Math.pow(mouseX - vm.x, 2) + Math.pow(mouseY - vm.y, 2)) < Math.pow(hoverRange, 2));
} else {
return false;
}
},
inLabelRange: function(mouseX) {
var vm = this._view;

View File

@ -55,15 +55,26 @@
},
inRange: function(mouseX, mouseY) {
var vm = this._view;
if (vm.y < vm.base) {
return (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2) && (mouseY >= vm.y && mouseY <= vm.base);
} else {
return (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2) && (mouseY >= vm.base && mouseY <= vm.y);
}
var inRange = false;
if (vm) {
if (vm.y < vm.base) {
inRange = (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2) && (mouseY >= vm.y && mouseY <= vm.base);
} else {
inRange = (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2) && (mouseY >= vm.base && mouseY <= vm.y);
}
}
return inRange;
},
inLabelRange: function(mouseX) {
var vm = this._view;
return (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2);
if (vm) {
return (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2);
} else {
return false;
}
},
tooltipPosition: function() {
var vm = this._view;

View File

@ -0,0 +1,385 @@
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', 3, 4, 5, 6],
objectProp: {
prop1: 'c',
prop2: 56,
prop3: 'prop3'
}
});
});
it('should merge arrays containing objects', function() {
var baseConfig = {
arrayProp: [{
prop1: 'abc',
prop2: 56
}],
};
var toMerge = {
arrayProp: [{
prop1: 'myProp1',
prop3: 'prop3'
}, 2, {
prop1: 'myProp1'
}],
};
var merged = helpers.configMerge(baseConfig, toMerge);
expect(merged).toEqual({
arrayProp: [{
prop1: 'myProp1',
prop2: 56,
prop3: 'prop3'
},
2, {
prop1: 'myProp1'
}],
});
});
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
}, {
type: 'linear',
display: true,
position: "right",
gridLines: {
show: true,
color: "rgba(0, 0, 0, 0.1)",
lineWidth: 1,
drawOnChartArea: true,
drawTicks: true,
zeroLineWidth: 1,
zeroLineColor: "rgba(0,0,0,0.25)",
},
reverse: false,
beginAtZero: false,
override: null,
labels: {
show: true,
mirror: false,
padding: 10,
template: "<%=value.toLocaleString()%>",
fontSize: 12,
fontStyle: "normal",
fontColor: "#666",
fontFamily: "Helvetica Neue"
}
}, {
type: 'linear',
display: true,
position: "left",
gridLines: {
show: true,
color: "rgba(0, 0, 0, 0.1)",
lineWidth: 1,
drawOnChartArea: true,
drawTicks: true,
zeroLineWidth: 1,
zeroLineColor: "rgba(0,0,0,0.25)",
},
reverse: false,
beginAtZero: false,
override: null,
labels: {
show: true,
mirror: false,
padding: 10,
template: "<%=value.toLocaleString()%>",
fontSize: 12,
fontStyle: "normal",
fontColor: "#666",
fontFamily: "Helvetica Neue"
}
}]
}
});
});
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)).toBe(3);
});
it ('Should generate ids', function() {
expect(helpers.uid()).toBe('chart-0');
expect(helpers.uid()).toBe('chart-1');
expect(helpers.uid()).toBe('chart-2');
expect(helpers.uid()).toBe('chart-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 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);
});
});

176
test/element.arc.tests.js Normal file
View File

@ -0,0 +1,176 @@
// Test the rectangle element
describe('Arc element tests', function() {
it ('Should be constructed', function() {
var arc = new Chart.elements.Arc({
_datasetIndex: 2,
_index: 1
});
expect(arc).not.toBe(undefined);
expect(arc._datasetIndex).toBe(2);
expect(arc._index).toBe(1);
});
it ('should determine if in range', function() {
var arc = new Chart.elements.Arc({
_datasetIndex: 2,
_index: 1
});
// Make sure we can run these before the view is added
expect(arc.inRange(2, 2)).toBe(false);
expect(arc.inLabelRange(2)).toBe(false);
// Mock out the view as if the controller put it there
arc._view = {
startAngle: 0,
endAngle: Math.PI / 2,
x: 0,
y: 0,
innerRadius: 5,
outerRadius: 10,
};
expect(arc.inRange(2, 2)).toBe(false);
expect(arc.inRange(7, 0)).toBe(true);
expect(arc.inRange(0, 11)).toBe(false);
expect(arc.inRange(Math.sqrt(32), Math.sqrt(32))).toBe(true);
expect(arc.inRange(-1.0 * Math.sqrt(7), Math.sqrt(7))).toBe(false);
});
it ('should get the tooltip position', function() {
var arc = new Chart.elements.Arc({
_datasetIndex: 2,
_index: 1
});
// Mock out the view as if the controller put it there
arc._view = {
startAngle: 0,
endAngle: Math.PI / 2,
x: 0,
y: 0,
innerRadius: 0,
outerRadius: Math.sqrt(2),
};
var pos = arc.tooltipPosition();
expect(pos.x).toBeCloseTo(0.5);
expect(pos.y).toBeCloseTo(0.5);
});
it ('should draw correctly with no border', function() {
var mockContext = window.createMockContext();
var arc = new Chart.elements.Arc({
_datasetIndex: 2,
_index: 1,
_chart: {
ctx: mockContext,
}
});
// Mock out the view as if the controller put it there
arc._view = {
startAngle: 0,
endAngle: Math.PI / 2,
x: 10,
y: 5,
innerRadius: 1,
outerRadius: 3,
backgroundColor: 'rgb(0, 0, 255)',
borderColor: 'rgb(255, 0, 0)',
};
arc.draw();
expect(mockContext.getCalls()).toEqual([{
name: 'beginPath',
args: []
}, {
name: 'arc',
args: [10, 5, 3, 0, Math.PI / 2]
}, {
name: 'arc',
args: [10, 5, 1, Math.PI / 2, 0, true]
}, {
name: 'closePath',
args: []
}, {
name: 'setStrokeStyle',
args: ['rgb(255, 0, 0)']
}, {
name: 'setLineWidth',
args: [undefined]
}, {
name: 'setFillStyle',
args: ['rgb(0, 0, 255)']
}, {
name: 'fill',
args: []
}, {
name: 'setLineJoin',
args: ['bevel']
}]);
});
it ('should draw correctly with a border', function() {
var mockContext = window.createMockContext();
var arc = new Chart.elements.Arc({
_datasetIndex: 2,
_index: 1,
_chart: {
ctx: mockContext,
}
});
// Mock out the view as if the controller put it there
arc._view = {
startAngle: 0,
endAngle: Math.PI / 2,
x: 10,
y: 5,
innerRadius: 1,
outerRadius: 3,
backgroundColor: 'rgb(0, 0, 255)',
borderColor: 'rgb(255, 0, 0)',
borderWidth: 5
};
arc.draw();
expect(mockContext.getCalls()).toEqual([{
name: 'beginPath',
args: []
}, {
name: 'arc',
args: [10, 5, 3, 0, Math.PI / 2]
}, {
name: 'arc',
args: [10, 5, 1, Math.PI / 2, 0, true]
}, {
name: 'closePath',
args: []
}, {
name: 'setStrokeStyle',
args: ['rgb(255, 0, 0)']
}, {
name: 'setLineWidth',
args: [5]
}, {
name: 'setFillStyle',
args: ['rgb(0, 0, 255)']
}, {
name: 'fill',
args: []
}, {
name: 'setLineJoin',
args: ['bevel']
}, {
name: 'stroke',
args: []
}]);
});
});

526
test/element.line.tests.js Normal file
View File

@ -0,0 +1,526 @@
// Tests for the line element
describe('Line element tests', function() {
it ('should be constructed', function() {
var line = new Chart.elements.Line({
_datasetindex: 2,
_points: [1, 2, 3, 4]
});
expect(line).not.toBe(undefined);
expect(line._datasetindex).toBe(2);
expect(line._points).toEqual([1, 2, 3, 4]);
});
it ('should draw with default settings', function() {
var mockContext = window.createMockContext();
// Create our points
var points = [];
points.push(new Chart.elements.Point({
_datasetindex: 2,
_index: 0,
_view: {
x: 0,
y: 10
}
}));
points.push(new Chart.elements.Point({
_datasetindex: 2,
_index: 1,
_view: {
x: 5,
y: 0
}
}));
points.push(new Chart.elements.Point({
_datasetindex: 2,
_index: 2,
_view: {
x: 15,
y: -10
}
}));
points.push(new Chart.elements.Point({
_datasetindex: 2,
_index: 3,
_view: {
x: 19,
y: -5
}
}));
var line = new Chart.elements.Line({
_datasetindex: 2,
_chart: {
ctx: mockContext,
},
_children: points,
// Need to provide some settings
_view: {
fill: false, // don't want to fill
tension: 0.0, // no bezier curve for now
}
})
line.draw();
expect(mockContext.getCalls()).toEqual([{
name: 'save',
args: [],
}, {
name: 'moveTo',
args: [0, 10]
}, {
name: 'lineTo',
args: [5, 0]
}, {
name: 'lineTo',
args: [15, -10]
}, {
name: 'lineTo',
args: [19, -5]
}, {
name: 'setLineCap',
args: ['butt']
}, {
name: 'setLineDash',
args: [[]]
}, {
name: 'setLineDashOffset',
args: [0.0]
}, {
name: 'setLineJoin',
args: ['miter']
}, {
name: 'setLineWidth',
args: [3]
}, {
name: 'setStrokeStyle',
args: ['rgba(0,0,0,0.1)']
}, {
name: 'beginPath',
args: []
}, {
name: 'moveTo',
args: [0, 10]
}, {
name: 'lineTo',
args: [5, 0]
}, {
name: 'lineTo',
args: [15, -10]
}, {
name: 'lineTo',
args: [19, -5]
}, {
name: 'stroke',
args: [],
}, {
name: 'restore',
args: []
}])
});
it ('should draw with custom settings', function() {
var mockContext = window.createMockContext();
// Create our points
var points = [];
points.push(new Chart.elements.Point({
_datasetindex: 2,
_index: 0,
_view: {
x: 0,
y: 10
}
}));
points.push(new Chart.elements.Point({
_datasetindex: 2,
_index: 1,
_view: {
x: 5,
y: 0
}
}));
points.push(new Chart.elements.Point({
_datasetindex: 2,
_index: 2,
_view: {
x: 15,
y: -10
}
}));
points.push(new Chart.elements.Point({
_datasetindex: 2,
_index: 3,
_view: {
x: 19,
y: -5
}
}));
var line = new Chart.elements.Line({
_datasetindex: 2,
_chart: {
ctx: mockContext,
},
_children: points,
// Need to provide some settings
_view: {
fill: true,
scaleZero: 2, // for filling lines
tension: 0.0, // no bezier curve for now
borderCapStyle: 'round',
borderColor: 'rgb(255, 255, 0)',
borderDash: [2, 2],
borderDashOffset: 1.5,
borderJoinStyle: 'bevel',
borderWidth: 4,
backgroundColor: 'rgb(0, 0, 0)'
}
})
line.draw();
var expected = [{
name: 'save',
args: [],
}, {
name: 'moveTo',
args: [0, 10]
}, {
name: 'lineTo',
args: [5, 0]
}, {
name: 'lineTo',
args: [15, -10]
}, {
name: 'lineTo',
args: [19, -5]
}, {
name: 'lineTo',
args: [19, 2]
}, {
name: 'lineTo',
args: [0, 2]
}, {
name: 'setFillStyle',
args: ['rgb(0, 0, 0)']
}, {
name: 'closePath',
args: []
}, {
name: 'fill',
args: []
}, {
name: 'setLineCap',
args: ['round']
}, {
name: 'setLineDash',
args: [[2, 2]]
}, {
name: 'setLineDashOffset',
args: [1.5]
}, {
name: 'setLineJoin',
args: ['bevel']
}, {
name: 'setLineWidth',
args: [4]
}, {
name: 'setStrokeStyle',
args: ['rgb(255, 255, 0)']
}, {
name: 'beginPath',
args: []
}, {
name: 'moveTo',
args: [0, 10]
}, {
name: 'lineTo',
args: [5, 0]
}, {
name: 'lineTo',
args: [15, -10]
}, {
name: 'lineTo',
args: [19, -5]
}, {
name: 'stroke',
args: [],
}, {
name: 'restore',
args: []
}];
expect(mockContext.getCalls()).toEqual(expected)
});
it ('should be able to draw with a loop back to the beginning point', function() {
var mockContext = window.createMockContext();
// Create our points
var points = [];
points.push(new Chart.elements.Point({
_datasetindex: 2,
_index: 0,
_view: {
x: 0,
y: 10
}
}));
points.push(new Chart.elements.Point({
_datasetindex: 2,
_index: 1,
_view: {
x: 5,
y: 0
}
}));
points.push(new Chart.elements.Point({
_datasetindex: 2,
_index: 2,
_view: {
x: 15,
y: -10
}
}));
points.push(new Chart.elements.Point({
_datasetindex: 2,
_index: 3,
_view: {
x: 19,
y: -5
}
}));
var line = new Chart.elements.Line({
_datasetindex: 2,
_chart: {
ctx: mockContext,
},
_children: points,
_loop: true, // want the line to loop back to the first point
// Need to provide some settings
_view: {
fill: false, // don't want to fill
tension: 0.0, // no bezier curve for now
}
})
line.draw();
expect(mockContext.getCalls()).toEqual([{
name: 'save',
args: [],
}, {
name: 'moveTo',
args: [0, 10]
}, {
name: 'lineTo',
args: [5, 0]
}, {
name: 'lineTo',
args: [15, -10]
}, {
name: 'lineTo',
args: [19, -5]
}, {
name: 'lineTo',
args: [0, 10]
}, {
name: 'setLineCap',
args: ['butt']
}, {
name: 'setLineDash',
args: [[]]
}, {
name: 'setLineDashOffset',
args: [0.0]
}, {
name: 'setLineJoin',
args: ['miter']
}, {
name: 'setLineWidth',
args: [3]
}, {
name: 'setStrokeStyle',
args: ['rgba(0,0,0,0.1)']
}, {
name: 'beginPath',
args: []
}, {
name: 'moveTo',
args: [0, 10]
}, {
name: 'lineTo',
args: [5, 0]
}, {
name: 'lineTo',
args: [15, -10]
}, {
name: 'lineTo',
args: [19, -5]
}, {
name: 'lineTo',
args: [0, 10]
}, {
name: 'stroke',
args: [],
}, {
name: 'restore',
args: []
}])
});
it ('should draw with bezier curves if tension > 0', function() {
var mockContext = window.createMockContext();
// Create our points
var points = [];
points.push(new Chart.elements.Point({
_datasetindex: 2,
_index: 0,
_view: {
x: 0,
y: 10,
controlPointNextX: 1,
controlPointNextY: 1,
controlPointPreviousX: 0,
controlPointPreviousY: 0,
}
}));
points.push(new Chart.elements.Point({
_datasetindex: 2,
_index: 1,
_view: {
x: 5,
y: 0,
controlPointNextX: 6,
controlPointNextY: 7,
controlPointPreviousX: 4,
controlPointPreviousY: 3,
}
}));
points.push(new Chart.elements.Point({
_datasetindex: 2,
_index: 2,
_view: {
x: 15,
y: -10,
controlPointNextX: 16,
controlPointNextY: 17,
controlPointPreviousX: 14,
controlPointPreviousY: 13,
}
}));
points.push(new Chart.elements.Point({
_datasetindex: 2,
_index: 3,
_view: {
x: 19,
y: -5,
controlPointNextX: 20,
controlPointNextY: 21,
controlPointPreviousX: 18,
controlPointPreviousY: 17,
}
}));
var line = new Chart.elements.Line({
_datasetindex: 2,
_chart: {
ctx: mockContext,
},
_children: points,
// Need to provide some settings
_view: {
fill: true,
scaleZero: 2, // for filling lines
tension: 0.5, // have bezier curves
borderCapStyle: 'round',
borderColor: 'rgb(255, 255, 0)',
borderDash: [2, 2],
borderDashOffset: 1.5,
borderJoinStyle: 'bevel',
borderWidth: 4,
backgroundColor: 'rgb(0, 0, 0)'
}
})
line.draw();
var expected = [{
name: 'save',
args: [],
}, {
name: 'moveTo',
args: [0, 10]
}, {
name: 'bezierCurveTo',
args: [1, 1, 4, 3, 5, 0]
}, {
name: 'bezierCurveTo',
args: [6, 7, 14, 13, 15, -10]
}, {
name: 'bezierCurveTo',
args: [16, 17, 18, 17, 19, -5]
}, {
name: 'lineTo',
args: [19, 2]
}, {
name: 'lineTo',
args: [0, 2]
}, {
name: 'setFillStyle',
args: ['rgb(0, 0, 0)']
}, {
name: 'closePath',
args: []
}, {
name: 'fill',
args: []
}, {
name: 'setLineCap',
args: ['round']
}, {
name: 'setLineDash',
args: [[2, 2]]
}, {
name: 'setLineDashOffset',
args: [1.5]
}, {
name: 'setLineJoin',
args: ['bevel']
}, {
name: 'setLineWidth',
args: [4]
}, {
name: 'setStrokeStyle',
args: ['rgb(255, 255, 0)']
}, {
name: 'beginPath',
args: []
}, {
name: 'moveTo',
args: [0, 10]
}, {
name: 'bezierCurveTo',
args: [1, 1, 4, 3, 5, 0]
}, {
name: 'bezierCurveTo',
args: [6, 7, 14, 13, 15, -10]
}, {
name: 'bezierCurveTo',
args: [16, 17, 18, 17, 19, -5]
}, {
name: 'stroke',
args: [],
}, {
name: 'restore',
args: []
}];
expect(mockContext.getCalls()).toEqual(expected)
});
});

190
test/element.point.tests.js Normal file
View File

@ -0,0 +1,190 @@
// Test the point element
describe('Point element tests', function() {
it ('Should be constructed', function() {
var point = new Chart.elements.Point({
_datasetIndex: 2,
_index: 1
});
expect(point).not.toBe(undefined);
expect(point._datasetIndex).toBe(2);
expect(point._index).toBe(1);
});
it ('Should correctly identify as in range', function() {
var point = new Chart.elements.Point({
_datasetIndex: 2,
_index: 1
});
// Safely handles if these are called before the viewmodel is instantiated
expect(point.inRange(5)).toBe(false);
expect(point.inLabelRange(5)).toBe(false);
// Attach a view object as if we were the controller
point._view = {
radius: 2,
hitRadius: 3,
x: 10,
y: 15
};
expect(point.inRange(10, 15)).toBe(true);
expect(point.inRange(10, 10)).toBe(false);
expect(point.inRange(10, 5)).toBe(false);
expect(point.inRange(5, 5)).toBe(false);
expect(point.inLabelRange(5)).toBe(false);
expect(point.inLabelRange(7)).toBe(true);
expect(point.inLabelRange(10)).toBe(true);
expect(point.inLabelRange(12)).toBe(true);
expect(point.inLabelRange(15)).toBe(false);
expect(point.inLabelRange(20)).toBe(false);
});
it ('should get the correct tooltip position', function() {
var point = new Chart.elements.Point({
_datasetIndex: 2,
_index: 1
});
// Attach a view object as if we were the controller
point._view = {
radius: 2,
borderWidth: 6,
x: 10,
y: 15
};
expect(point.tooltipPosition()).toEqual({
x: 10,
y: 15,
padding: 8
});
});
it ('should draw correctly', function() {
var mockContext = window.createMockContext();
var point = new Chart.elements.Point({
_datasetIndex: 2,
_index: 1,
_chart: {
ctx: mockContext,
}
});
// Attach a view object as if we were the controller
point._view = {
radius: 2,
hitRadius: 3,
borderColor: 'rgba(1, 2, 3, 1)',
borderWidth: 6,
backgroundColor: 'rgba(0, 255, 0)',
x: 10,
y: 15,
ctx: mockContext
};
point.draw();
expect(mockContext.getCalls()).toEqual([{
name: 'beginPath',
args: []
}, {
name: 'arc',
args: [10, 15, 2, 0, 2 * Math.PI]
}, {
name: 'closePath',
args: [],
}, {
name: 'setStrokeStyle',
args: ['rgba(1, 2, 3, 1)']
}, {
name: 'setLineWidth',
args: [6]
}, {
name: 'setFillStyle',
args: ['rgba(0, 255, 0)']
}, {
name: 'fill',
args: [],
}, {
name: 'stroke',
args: []
}]);
});
it ('should draw correctly with default settings if necessary', function() {
var mockContext = window.createMockContext();
var point = new Chart.elements.Point({
_datasetIndex: 2,
_index: 1,
_chart: {
ctx: mockContext,
}
});
// Attach a view object as if we were the controller
point._view = {
radius: 2,
hitRadius: 3,
x: 10,
y: 15,
ctx: mockContext
};
point.draw();
expect(mockContext.getCalls()).toEqual([{
name: 'beginPath',
args: []
}, {
name: 'arc',
args: [10, 15, 2, 0, 2 * Math.PI]
}, {
name: 'closePath',
args: [],
}, {
name: 'setStrokeStyle',
args: ['rgba(0,0,0,0.1)']
}, {
name: 'setLineWidth',
args: [1]
}, {
name: 'setFillStyle',
args: ['rgba(0,0,0,0.1)']
}, {
name: 'fill',
args: [],
}, {
name: 'stroke',
args: []
}]);
});
it ('should not draw if skipped', function() {
var mockContext = window.createMockContext();
var point = new Chart.elements.Point({
_datasetIndex: 2,
_index: 1,
_chart: {
ctx: mockContext,
}
});
// Attach a view object as if we were the controller
point._view = {
radius: 2,
hitRadius: 3,
x: 10,
y: 15,
ctx: mockContext,
skip: true
};
point.draw();
expect(mockContext.getCalls()).toEqual([]);
});
});

View File

@ -0,0 +1,246 @@
// Test the rectangle element
describe('Rectangle element tests', function() {
it ('Should be constructed', function() {
var rectangle = new Chart.elements.Rectangle({
_datasetIndex: 2,
_index: 1
});
expect(rectangle).not.toBe(undefined);
expect(rectangle._datasetIndex).toBe(2);
expect(rectangle._index).toBe(1);
});
it ('Should correctly identify as in range', function() {
var rectangle = new Chart.elements.Rectangle({
_datasetIndex: 2,
_index: 1
});
// Safely handles if these are called before the viewmodel is instantiated
expect(rectangle.inRange(5)).toBe(false);
expect(rectangle.inLabelRange(5)).toBe(false);
// Attach a view object as if we were the controller
rectangle._view = {
base: 0,
width: 4,
x: 10,
y: 15
};
expect(rectangle.inRange(10, 15)).toBe(true);
expect(rectangle.inRange(10, 10)).toBe(true);
expect(rectangle.inRange(10, 16)).toBe(false);
expect(rectangle.inRange(5, 5)).toBe(false);
expect(rectangle.inLabelRange(5)).toBe(false);
expect(rectangle.inLabelRange(7)).toBe(false);
expect(rectangle.inLabelRange(10)).toBe(true);
expect(rectangle.inLabelRange(12)).toBe(true);
expect(rectangle.inLabelRange(15)).toBe(false);
expect(rectangle.inLabelRange(20)).toBe(false);
// Test when the y is below the base (negative bar)
var negativeRectangle = new Chart.elements.Rectangle({
_datasetIndex: 2,
_index: 1
});
// Attach a view object as if we were the controller
negativeRectangle._view = {
base: 0,
width: 4,
x: 10,
y: -15
};
expect(negativeRectangle.inRange(10, -16)).toBe(false);
expect(negativeRectangle.inRange(10, 1)).toBe(false);
expect(negativeRectangle.inRange(10, -5)).toBe(true);
});
it ('should get the correct height', function() {
var rectangle = new Chart.elements.Rectangle({
_datasetIndex: 2,
_index: 1
});
// Attach a view object as if we were the controller
rectangle._view = {
base: 0,
width: 4,
x: 10,
y: 15
};
expect(rectangle.height()).toBe(-15);
// Test when the y is below the base (negative bar)
var negativeRectangle = new Chart.elements.Rectangle({
_datasetIndex: 2,
_index: 1
});
// Attach a view object as if we were the controller
negativeRectangle._view = {
base: -10,
width: 4,
x: 10,
y: -15
};
expect(negativeRectangle.height()).toBe(5);
});
it ('should get the correct tooltip position', function() {
var rectangle = new Chart.elements.Rectangle({
_datasetIndex: 2,
_index: 1
});
// Attach a view object as if we were the controller
rectangle._view = {
base: 0,
width: 4,
x: 10,
y: 15
};
expect(rectangle.tooltipPosition()).toEqual({
x: 10,
y: 0,
});
// Test when the y is below the base (negative bar)
var negativeRectangle = new Chart.elements.Rectangle({
_datasetIndex: 2,
_index: 1
});
// Attach a view object as if we were the controller
negativeRectangle._view = {
base: -10,
width: 4,
x: 10,
y: -15
};
expect(negativeRectangle.tooltipPosition()).toEqual({
x: 10,
y: -15,
});
});
it ('should draw correctly', function() {
var mockContext = window.createMockContext();
var rectangle = new Chart.elements.Rectangle({
_datasetIndex: 2,
_index: 1,
_chart: {
ctx: mockContext,
}
});
// Attach a view object as if we were the controller
rectangle._view = {
backgroundColor: 'rgb(255, 0, 0)',
base: 0,
borderColor: 'rgb(0, 0, 255)',
borderWidth: 1,
ctx: mockContext,
width: 4,
x: 10,
y: 15,
};
rectangle.draw();
expect(mockContext.getCalls()).toEqual([{
name: 'beginPath',
args: [],
}, {
name: 'setFillStyle',
args: ['rgb(255, 0, 0)']
}, {
name: 'setStrokeStyle',
args: ['rgb(0, 0, 255)'],
}, {
name: 'setLineWidth',
args: [1]
}, {
name: 'moveTo',
args: [8.5, 0]
}, {
name: 'lineTo',
args: [8.5, 15.5]
}, {
name: 'lineTo',
args: [11.5, 15.5]
}, {
name: 'lineTo',
args: [11.5, 0]
}, {
name: 'fill',
args: [],
}, {
name: 'stroke',
args: []
}]);
});
it ('should draw correctly with no stroke', function() {
var mockContext = window.createMockContext();
var rectangle = new Chart.elements.Rectangle({
_datasetIndex: 2,
_index: 1,
_chart: {
ctx: mockContext,
}
});
// Attach a view object as if we were the controller
rectangle._view = {
backgroundColor: 'rgb(255, 0, 0)',
base: 0,
borderColor: 'rgb(0, 0, 255)',
ctx: mockContext,
width: 4,
x: 10,
y: 15,
};
rectangle.draw();
expect(mockContext.getCalls()).toEqual([{
name: 'beginPath',
args: [],
}, {
name: 'setFillStyle',
args: ['rgb(255, 0, 0)']
}, {
name: 'setStrokeStyle',
args: ['rgb(0, 0, 255)'],
}, {
name: 'setLineWidth',
args: [undefined]
}, {
name: 'moveTo',
args: [8, 0]
}, {
name: 'lineTo',
args: [8, 15]
}, {
name: 'lineTo',
args: [12, 15]
}, {
name: 'lineTo',
args: [12, 0]
}, {
name: 'fill',
args: [],
}]);
});
});

108
test/mockContext.js Normal file
View File

@ -0,0 +1,108 @@
(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() {},
closePath: function() {},
fill: function() {},
lineTo: function(x, y) {},
moveTo: function(x, y) {},
restore: function() {},
save: function() {},
setLineDash: function() {},
stroke: function() {}
};
// attach methods to the class itself
var scope = this;
var addMethod = function(name, method) {
scope[methodName] = function() {
scope.record(name, arguments);
method.apply(scope, arguments);
};
}
for (var 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;
}
window.createMockContext = function() {
return new Context();
};
})();