@netdata/charts
Version:
Netdata frontend SDK and chart utilities
224 lines (221 loc) • 8.75 kB
JavaScript
import crosshair from "./crosshair";
import { makeTestChart } from "@jest/testUtilities";
describe("crosshair", function () {
var chart, mockChartUI, mockDygraph, mockCtx;
beforeEach(function () {
var _makeTestChart = makeTestChart({
libraryName: "dygraph"
}),
testChart = _makeTestChart.chart;
chart = testChart;
jest.spyOn(chart, "getPayload").mockReturnValue({
data: [[1617946860000, 25, 50, 75], [1617946920000, 30, 55, 70], [1617946980000, 20, 45, 80], [1617947040000, 35, 60, 65]]
});
mockCtx = {
save: jest.fn(),
restore: jest.fn(),
beginPath: jest.fn(),
closePath: jest.fn(),
moveTo: jest.fn(),
lineTo: jest.fn(),
stroke: jest.fn(),
setLineDash: jest.fn(),
strokeStyle: ""
};
mockDygraph = {
getArea: jest.fn(function () {
return {
h: 400
};
}),
canvas_ctx_: mockCtx,
toDomXCoord: jest.fn(function (timestamp) {
return timestamp / 1000000;
}),
setSelection: jest.fn()
};
mockChartUI = {
getDygraph: jest.fn(function () {
return mockDygraph;
}),
chart: chart
};
});
afterEach(function () {
jest.clearAllMocks();
});
describe("basic functionality", function () {
it("draws crosshair for valid row", function () {
crosshair(mockChartUI, 1);
expect(mockDygraph.getArea).toHaveBeenCalled();
expect(mockDygraph.toDomXCoord).toHaveBeenCalledWith(1617946920000);
expect(mockDygraph.setSelection).toHaveBeenCalledWith(1);
expect(mockCtx.save).toHaveBeenCalled();
expect(mockCtx.restore).toHaveBeenCalled();
expect(mockCtx.beginPath).toHaveBeenCalled();
expect(mockCtx.closePath).toHaveBeenCalled();
expect(mockCtx.stroke).toHaveBeenCalled();
});
it("sets correct line dash for default hover", function () {
crosshair(mockChartUI, 0);
expect(mockCtx.setLineDash).toHaveBeenCalledWith([5, 5]);
});
it("draws line from top to bottom of chart area", function () {
var mockX = 1617.94692; // Expected x coordinate
mockDygraph.toDomXCoord.mockReturnValue(mockX);
crosshair(mockChartUI, 0);
expect(mockCtx.moveTo).toHaveBeenCalledWith(mockX, 0);
expect(mockCtx.lineTo).toHaveBeenCalledWith(mockX, 400);
});
it("uses correct stroke style for hover", function () {
var getThemeAttributeSpy = jest.spyOn(chart, "getThemeAttribute").mockReturnValue("#ff6600");
crosshair(mockChartUI, 0, "hover");
expect(getThemeAttributeSpy).toHaveBeenCalledWith("themeCrosshair");
expect(mockCtx.strokeStyle).toBe("#ff6600");
getThemeAttributeSpy.mockRestore();
});
});
describe("flavour variations", function () {
it("handles hover flavour", function () {
var getThemeAttributeSpy = jest.spyOn(chart, "getThemeAttribute");
crosshair(mockChartUI, 1, "hover");
expect(mockCtx.setLineDash).toHaveBeenCalledWith([5, 5]);
expect(getThemeAttributeSpy).toHaveBeenCalledWith("themeCrosshair");
getThemeAttributeSpy.mockRestore();
});
it("handles click flavour", function () {
var getThemeAttributeSpy = jest.spyOn(chart, "getThemeAttribute");
crosshair(mockChartUI, 1, "click");
expect(mockCtx.setLineDash).toHaveBeenCalledWith([2, 2]);
expect(getThemeAttributeSpy).toHaveBeenCalledWith("themeNetdata");
getThemeAttributeSpy.mockRestore();
});
it("defaults to hover flavour when no flavour specified", function () {
var getThemeAttributeSpy = jest.spyOn(chart, "getThemeAttribute");
crosshair(mockChartUI, 1);
expect(mockCtx.setLineDash).toHaveBeenCalledWith([5, 5]);
expect(getThemeAttributeSpy).toHaveBeenCalledWith("themeCrosshair");
getThemeAttributeSpy.mockRestore();
});
it("handles unknown flavour by using undefined values", function () {
var getThemeAttributeSpy = jest.spyOn(chart, "getThemeAttribute");
crosshair(mockChartUI, 1, "unknown");
expect(mockCtx.setLineDash).toHaveBeenCalledWith(undefined);
expect(getThemeAttributeSpy).toHaveBeenCalledWith(undefined);
getThemeAttributeSpy.mockRestore();
});
});
describe("edge cases", function () {
it("handles invalid row index gracefully", function () {
crosshair(mockChartUI, 999); // Row index out of bounds
// Should return early and not call drawing methods
expect(mockCtx.save).not.toHaveBeenCalled();
expect(mockCtx.beginPath).not.toHaveBeenCalled();
});
it("handles empty data array", function () {
chart.getPayload.mockReturnValue({
data: []
});
crosshair(mockChartUI, 0);
expect(mockCtx.save).not.toHaveBeenCalled();
expect(mockCtx.beginPath).not.toHaveBeenCalled();
});
it("returns early for non-array row data", function () {
chart.getPayload.mockReturnValue({
data: [null, [1617946920000, 30, 55, 70]]
});
crosshair(mockChartUI, 0);
expect(mockCtx.save).not.toHaveBeenCalled();
expect(mockCtx.beginPath).not.toHaveBeenCalled();
});
it("handles missing payload data", function () {
chart.getPayload.mockReturnValue({
data: undefined
});
expect(function () {
return crosshair(mockChartUI, 0);
}).toThrow();
});
it("handles negative row index", function () {
crosshair(mockChartUI, -1);
// Should return early and not call drawing methods
expect(mockCtx.save).not.toHaveBeenCalled();
expect(mockCtx.beginPath).not.toHaveBeenCalled();
});
});
describe("canvas context operations", function () {
it("saves and restores canvas context", function () {
crosshair(mockChartUI, 1);
expect(mockCtx.save).toHaveBeenCalled();
expect(mockCtx.restore).toHaveBeenCalled();
});
it("properly sequences drawing operations", function () {
crosshair(mockChartUI, 1);
var calls = [mockCtx.save, mockCtx.beginPath, mockCtx.setLineDash, mockCtx.moveTo, mockCtx.lineTo, mockCtx.stroke, mockCtx.closePath, mockCtx.restore];
// Verify all drawing methods were called
calls.forEach(function (call) {
expect(call).toHaveBeenCalled();
});
});
});
describe("coordinate calculations", function () {
it("uses timestamp from correct data row", function () {
crosshair(mockChartUI, 2);
expect(mockDygraph.toDomXCoord).toHaveBeenCalledWith(1617946980000);
});
it("handles different chart area heights", function () {
mockDygraph.getArea.mockReturnValue({
h: 200
});
var mockX = 500;
mockDygraph.toDomXCoord.mockReturnValue(mockX);
crosshair(mockChartUI, 1);
expect(mockCtx.moveTo).toHaveBeenCalledWith(mockX, 0);
expect(mockCtx.lineTo).toHaveBeenCalledWith(mockX, 200);
});
it("handles zero height chart area", function () {
mockDygraph.getArea.mockReturnValue({
h: 0
});
var mockX = 300;
mockDygraph.toDomXCoord.mockReturnValue(mockX);
crosshair(mockChartUI, 0);
expect(mockCtx.moveTo).toHaveBeenCalledWith(mockX, 0);
expect(mockCtx.lineTo).toHaveBeenCalledWith(mockX, 0);
});
});
describe("dygraph integration", function () {
it("sets dygraph selection to correct row", function () {
crosshair(mockChartUI, 3);
expect(mockDygraph.setSelection).toHaveBeenCalledWith(3);
});
it("gets chart area from dygraph", function () {
crosshair(mockChartUI, 0);
expect(mockDygraph.getArea).toHaveBeenCalled();
});
it("converts timestamp to DOM coordinates", function () {
crosshair(mockChartUI, 1);
expect(mockDygraph.toDomXCoord).toHaveBeenCalledWith(1617946920000);
});
});
describe("theme integration", function () {
it("retrieves theme color for hover", function () {
var getThemeAttributeSpy = jest.spyOn(chart, "getThemeAttribute");
crosshair(mockChartUI, 0, "hover");
expect(getThemeAttributeSpy).toHaveBeenCalledWith("themeCrosshair");
getThemeAttributeSpy.mockRestore();
});
it("retrieves theme color for click", function () {
var getThemeAttributeSpy = jest.spyOn(chart, "getThemeAttribute");
crosshair(mockChartUI, 0, "click");
expect(getThemeAttributeSpy).toHaveBeenCalledWith("themeNetdata");
getThemeAttributeSpy.mockRestore();
});
it("handles missing theme attributes gracefully", function () {
var getThemeAttributeSpy = jest.spyOn(chart, "getThemeAttribute").mockReturnValue(undefined);
crosshair(mockChartUI, 0);
expect(mockCtx.strokeStyle).toBeUndefined();
getThemeAttributeSpy.mockRestore();
});
});
});