homeio
PublikFork dari doctor-io/homeio
metric-history-chart.test.tsx
/* @vitest-environment jsdom */
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import type { MetricsHistoryPoint } from "@/lib/shared/contracts/system";
import { MetricHistoryChart } from "@/modules/system/components/metric-history-chart";
const HOUR = 60 * 60 * 1000;
const NOW = Date.UTC(2026, 8, 27, 12, 0, 0);
function point(t: number, cpu: number | null): MetricsHistoryPoint {
return { t, cpuPercent: cpu, memoryPercent: null, temperatureCelsius: null, downloadMbps: null, uploadMbps: null, diskReadMBps: null, diskWriteMBps: null };
}
function renderChart(points: MetricsHistoryPoint[], { fitToData = false } = {}) {
return render(
<MetricHistoryChart
points={points}
series={[{ key: "cpuPercent", label: "CPU", className: "text-primary" }]}
rangeMs={HOUR}
now={NOW}
gapMs={15_000}
max={100}
rangeLabel="1 h"
formatValue={(v) => `${v.toFixed(1)}%`}
formatTime={(t) => new Date(t).toISOString().slice(11, 19)}
fitToData={fitToData}
/>,
);
}
describe("MetricHistoryChart", () => {
it("says it is collecting until there is a reading", () => {
renderChart([point(NOW - 5_000, null)]);
expect(screen.getByText("Collecting…")).toBeTruthy();
expect(screen.queryByRole("img")).toBeNull();
});
it("draws a full hour and labels its span", () => {
const points = Array.from({ length: 720 }, (_, i) => point(NOW - HOUR + (i + 1) * 5_000, i % 100));
const { container } = renderChart(points);
expect(screen.getByRole("img", { name: "CPU over the last 1 h" })).toBeTruthy();
expect(screen.getByText("1 h ago")).toBeTruthy();
const line = container.querySelectorAll("path")[1].getAttribute("d") ?? "";
expect(line.match(/M/g)).toHaveLength(1);
});
it("breaks the line across a gap, such as a restart, and says since when history exists", () => {
const points = [
point(NOW - 20 * 60_000, 10),
point(NOW - 20 * 60_000 + 5_000, 12),
// Homeio restarted: nothing for ten minutes.
point(NOW - 10 * 60_000, 30),
point(NOW - 10 * 60_000 + 5_000, 32),
];
const { container } = renderChart(points, { fitToData: true });
const line = container.querySelectorAll("path")[1].getAttribute("d") ?? "";
expect(line.match(/M/g)).toHaveLength(2);
expect(screen.getByText(`Since ${new Date(NOW - 20 * 60_000).toISOString().slice(11, 19)}`)).toBeTruthy();
});
it("spreads a short history across the full width in the 15 min view", () => {
// Two minutes of data in a one-hour chart.
const points = Array.from({ length: 24 }, (_, i) => point(NOW - 120_000 + (i + 1) * 5_000, 50));
const { container } = renderChart(points, { fitToData: true });
const line = container.querySelectorAll("path")[1].getAttribute("d") ?? "";
const xs = [...line.matchAll(/(-?[\d.]+),(-?[\d.]+)/g)].map((m) => Number(m[1]));
expect(Math.min(...xs)).toBeLessThan(15);
expect(Math.max(...xs)).toBeCloseTo(300, 0);
});
it("keeps the real time scale for 1 h and greys out the part with no data yet", () => {
const points = Array.from({ length: 24 }, (_, i) => point(NOW - 120_000 + (i + 1) * 5_000, 50));
const { container } = renderChart(points);
const line = container.querySelectorAll("path")[1].getAttribute("d") ?? "";
const xs = [...line.matchAll(/(-?[\d.]+),(-?[\d.]+)/g)].map((m) => Number(m[1]));
// Two minutes of an hour: the last thirtieth of the width.
expect(Math.min(...xs)).toBeGreaterThan(285);
expect(Math.max(...xs)).toBeCloseTo(300, 0);
expect(screen.getByText("No data yet")).toBeTruthy();
expect(screen.getByText(/^1 h ago · data since /)).toBeTruthy();
});
it("draws smooth curves that never overshoot a peak or dip below zero", () => {
const values = [0, 0, 100, 0, 0, 40, 60, 10];
const points = values.map((v, i) => point(NOW - (values.length - i) * 5_000, v));
const { container } = renderChart(points, { fitToData: true });
const line = container.querySelectorAll("path")[1].getAttribute("d") ?? "";
expect(line).toContain("C");
const ys = [...line.matchAll(/(-?[\d.]+),(-?[\d.]+)/g)].map((m) => Number(m[2]));
// 100 % is y = 0 at the top of the 64-unit chart, 0 % is y = 64 at the bottom.
expect(Math.min(...ys)).toBeGreaterThanOrEqual(0);
expect(Math.max(...ys)).toBeLessThanOrEqual(64);
});
it("shows the nearest reading on hover", () => {
const points = [point(NOW - 10_000, 42), point(NOW - 5_000, 43)];
const { container } = renderChart(points);
const area = container.firstElementChild?.firstElementChild as HTMLElement;
area.getBoundingClientRect = () => ({ left: 0, width: 300, top: 0, height: 64, right: 300, bottom: 64, x: 0, y: 0, toJSON: () => ({}) });
fireEvent.mouseMove(area, { clientX: 300 });
expect(screen.getByText("43.0%")).toBeTruthy();
});
});