From aa13c6fa53afc6e49c2ef48bed633cc7b618759f Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Mon, 9 Feb 2026 15:07:11 +0100 Subject: [PATCH] Add tests for energy chart functions (#29504) --- .../common/energy-chart-options.test.ts | 340 +++++++++++++++++- 1 file changed, 338 insertions(+), 2 deletions(-) diff --git a/test/panels/lovelace/cards/energy/common/energy-chart-options.test.ts b/test/panels/lovelace/cards/energy/common/energy-chart-options.test.ts index 0f1a7dc53c..7ac7750519 100644 --- a/test/panels/lovelace/cards/energy/common/energy-chart-options.test.ts +++ b/test/panels/lovelace/cards/energy/common/energy-chart-options.test.ts @@ -1,7 +1,12 @@ import { assert, describe, it } from "vitest"; -import type { LineSeriesOption } from "echarts/charts"; +import type { BarSeriesOption, LineSeriesOption } from "echarts/charts"; -import { fillLineGaps } from "../../../../../../src/panels/lovelace/cards/energy/common/energy-chart-options"; +import { + fillDataGapsAndRoundCaps, + fillLineGaps, + getCompareTransform, + getSuggestedMax, +} from "../../../../../../src/panels/lovelace/cards/energy/common/energy-chart-options"; // Helper to get x value from either [x,y] or {value: [x,y]} format function getX(item: any): number { @@ -13,6 +18,76 @@ function getY(item: any): number { return item?.value?.[1] ?? item?.[1]; } +describe("getSuggestedMax", () => { + it("returns end date unchanged for 5minute period", () => { + const end = new Date("2024-03-15T14:37:22.000"); + const result = getSuggestedMax("5minute", end, false); + assert.equal(result.getTime(), end.getTime()); + }); + + it("returns end date unchanged when noRounding is true", () => { + const end = new Date("2024-03-15T14:37:22.000"); + const result = getSuggestedMax("hour", end, true); + assert.equal(result.getTime(), end.getTime()); + }); + + it("rounds down to start of hour for hour period", () => { + const end = new Date("2024-03-15T14:37:22.000"); + const result = getSuggestedMax("hour", end, false); + assert.equal(result.getMinutes(), 0); + assert.equal(result.getSeconds(), 0); + assert.equal(result.getMilliseconds(), 0); + assert.equal(result.getHours(), 14); + }); + + it("rounds down to start of day for day period", () => { + const end = new Date("2024-03-15T14:37:22.000"); + const result = getSuggestedMax("day", end, false); + assert.equal(result.getHours(), 0); + assert.equal(result.getMinutes(), 0); + assert.equal(result.getDate(), 15); + }); + + it("rounds down to start of day for week period", () => { + const end = new Date("2024-03-15T14:37:22.000"); + const result = getSuggestedMax("week", end, false); + assert.equal(result.getHours(), 0); + assert.equal(result.getMinutes(), 0); + assert.equal(result.getDate(), 15); + }); + + it("rounds down to start of month for month period", () => { + const end = new Date("2024-03-15T14:37:22.000"); + const result = getSuggestedMax("month", end, false); + assert.equal(result.getDate(), 1); + assert.equal(result.getHours(), 0); + assert.equal(result.getMonth(), 2); // March = 2 + }); + + it("corrects DST edge case when hour is 0 for day period", () => { + // Simulate a time that lands exactly on midnight (e.g. DST adjustment) + const end = new Date("2024-03-15T00:00:00.000"); + const result = getSuggestedMax("day", end, false); + // Should subtract an hour first, landing on previous day + assert.equal(result.getDate(), 14); + assert.equal(result.getHours(), 0); + }); + + it("does not apply DST correction when hour is nonzero", () => { + const end = new Date("2024-03-15T10:30:00.000"); + const result = getSuggestedMax("day", end, false); + assert.equal(result.getDate(), 15); + assert.equal(result.getHours(), 0); + }); + + it("does not mutate the input date", () => { + const end = new Date("2024-03-15T14:37:22.000"); + const originalTime = end.getTime(); + getSuggestedMax("month", end, false); + assert.equal(end.getTime(), originalTime); + }); +}); + describe("fillLineGaps", () => { it("fills gaps in datasets with missing timestamps", () => { const datasets: LineSeriesOption[] = [ @@ -197,3 +272,264 @@ describe("fillLineGaps", () => { assert.equal(secondItem.itemStyle.color, "red"); }); }); + +// Helper to get bar data item +function getBarItem(dataset: BarSeriesOption, index: number): any { + const dp = dataset.data![index]; + return dp && typeof dp === "object" && "value" in dp ? dp : { value: dp }; +} + +describe("fillDataGapsAndRoundCaps", () => { + it("fills missing buckets with zero values", () => { + // When a dataset has entries at some but not all bucket positions, + // the function splices in zero-value entries to align them + const datasets: BarSeriesOption[] = [ + { + type: "bar", + stack: "a", + data: [ + [1000, 10], + [3000, 30], + ], + }, + { + type: "bar", + stack: "a", + data: [ + [1000, 100], + [2000, 200], + [3000, 300], + ], + }, + ]; + + fillDataGapsAndRoundCaps(datasets); + + // First dataset should now have 3 entries with bucket 2000 filled in + assert.equal(datasets[0].data!.length, 3); + const filled = getBarItem(datasets[0], 1); + assert.equal(filled.value[0], 2000); + assert.equal(filled.value[1], 0); + assert.equal(filled.itemStyle.borderWidth, 0); + }); + + it("sets borderWidth 0 on zero-value existing entries", () => { + const datasets: BarSeriesOption[] = [ + { + type: "bar", + stack: "a", + data: [ + [1000, 0], + [2000, 5], + ], + }, + ]; + + fillDataGapsAndRoundCaps(datasets); + + const zeroItem = getBarItem(datasets[0], 0); + assert.equal(zeroItem.itemStyle.borderWidth, 0); + }); + + it("rounds caps on top positive bar in stack", () => { + const datasets: BarSeriesOption[] = [ + { + type: "bar", + stack: "a", + data: [[1000, 10]], + }, + { + type: "bar", + stack: "a", + data: [[1000, 20]], + }, + ]; + + fillDataGapsAndRoundCaps(datasets); + + // Last dataset (topmost positive) gets rounded top caps + // Iteration is reverse so first positive hit from the end gets the cap + const topItem = getBarItem(datasets[1], 0); + assert.deepEqual(topItem.itemStyle.borderRadius, [4, 4, 0, 0]); + + // Bottom dataset should NOT have rounded caps + const bottomItem = getBarItem(datasets[0], 0); + assert.equal(bottomItem.itemStyle?.borderRadius, undefined); + }); + + it("rounds caps on bottom negative bar in stack", () => { + const datasets: BarSeriesOption[] = [ + { + type: "bar", + stack: "a", + data: [[1000, -10]], + }, + { + type: "bar", + stack: "a", + data: [[1000, -20]], + }, + ]; + + fillDataGapsAndRoundCaps(datasets); + + // Last dataset (bottommost negative) gets rounded bottom caps + const bottomItem = getBarItem(datasets[1], 0); + assert.deepEqual(bottomItem.itemStyle.borderRadius, [0, 0, 4, 4]); + + // First dataset should NOT have rounded caps + const topItem = getBarItem(datasets[0], 0); + assert.equal(topItem.itemStyle?.borderRadius, undefined); + }); + + it("handles different stacks independently", () => { + const datasets: BarSeriesOption[] = [ + { + type: "bar", + stack: "a", + data: [[1000, 10]], + }, + { + type: "bar", + stack: "b", + data: [[1000, 20]], + }, + ]; + + fillDataGapsAndRoundCaps(datasets); + + // Both should get caps since they're in different stacks + const itemA = getBarItem(datasets[0], 0); + assert.deepEqual(itemA.itemStyle.borderRadius, [4, 4, 0, 0]); + + const itemB = getBarItem(datasets[1], 0); + assert.deepEqual(itemB.itemStyle.borderRadius, [4, 4, 0, 0]); + }); + + it("handles object-format data items", () => { + const datasets: BarSeriesOption[] = [ + { + type: "bar", + stack: "a", + data: [{ value: [1000, 10] }], + }, + { + type: "bar", + stack: "a", + data: [{ value: [2000, 20] }], + }, + ]; + + fillDataGapsAndRoundCaps(datasets); + + // Both datasets should now have 2 entries + assert.equal(datasets[0].data!.length, 2); + assert.equal(datasets[1].data!.length, 2); + }); + + it("handles empty datasets", () => { + const datasets: BarSeriesOption[] = [ + { + type: "bar", + stack: "a", + data: [], + }, + ]; + + fillDataGapsAndRoundCaps(datasets); + + assert.equal(datasets[0].data!.length, 0); + }); +}); + +describe("getCompareTransform", () => { + it("returns identity transform when no compareStart", () => { + const start = new Date("2024-03-01"); + const transform = getCompareTransform(start); + + const testDate = new Date("2024-03-15T12:00:00"); + assert.equal(transform(testDate).getTime(), testDate.getTime()); + }); + + it("returns identity transform when compareStart is undefined", () => { + const start = new Date("2024-03-01"); + const transform = getCompareTransform(start, undefined); + + const testDate = new Date("2024-03-15T12:00:00"); + assert.equal(transform(testDate).getTime(), testDate.getTime()); + }); + + it("shifts by years when start is at year boundary", () => { + const start = new Date("2024-01-01T00:00:00"); + const compareStart = new Date("2023-01-01T00:00:00"); + const transform = getCompareTransform(start, compareStart); + + const testDate = new Date("2023-06-15T12:00:00"); + const result = transform(testDate); + // Should shift forward by 1 year + assert.equal(result.getFullYear(), 2024); + assert.equal(result.getMonth(), 5); // June + assert.equal(result.getDate(), 15); + }); + + it("shifts by months when start is at month boundary", () => { + const start = new Date("2024-03-01T00:00:00"); + const compareStart = new Date("2024-01-01T00:00:00"); + const transform = getCompareTransform(start, compareStart); + + const testDate = new Date("2024-01-15T12:00:00"); + const result = transform(testDate); + // Should shift forward by 2 months + assert.equal(result.getMonth(), 2); // March + assert.equal(result.getDate(), 15); + }); + + it("shifts by days when start is at day boundary", () => { + const start = new Date("2024-03-15T00:00:00"); + const compareStart = new Date("2024-03-08T00:00:00"); + const transform = getCompareTransform(start, compareStart); + + const testDate = new Date("2024-03-08T14:30:00"); + const result = transform(testDate); + // Should shift forward by 7 days + assert.equal(result.getDate(), 15); + assert.equal(result.getHours(), 14); + assert.equal(result.getMinutes(), 30); + }); + + it("falls back to millisecond offset for non-aligned starts", () => { + const start = new Date("2024-03-15T10:30:00"); + const compareStart = new Date("2024-03-14T10:30:00"); + const transform = getCompareTransform(start, compareStart); + + const testDate = new Date("2024-03-14T12:00:00"); + const result = transform(testDate); + const expectedOffset = start.getTime() - compareStart.getTime(); + assert.equal(result.getTime(), testDate.getTime() + expectedOffset); + }); + + it("prefers year shift over month shift when both apply", () => { + // Jan 1 is both start-of-year and start-of-month + const start = new Date("2024-01-01T00:00:00"); + const compareStart = new Date("2022-01-01T00:00:00"); + const transform = getCompareTransform(start, compareStart); + + const testDate = new Date("2022-07-01T00:00:00"); + const result = transform(testDate); + // Should shift by 2 years (year check comes first) + assert.equal(result.getFullYear(), 2024); + assert.equal(result.getMonth(), 6); // July + }); + + it("uses month shift when start is at month but not year boundary", () => { + const start = new Date("2024-06-01T00:00:00"); + const compareStart = new Date("2024-03-01T00:00:00"); + const transform = getCompareTransform(start, compareStart); + + const testDate = new Date("2024-03-20T08:00:00"); + const result = transform(testDate); + // Should shift by 3 months + assert.equal(result.getMonth(), 5); // June + assert.equal(result.getDate(), 20); + }); +});