mirror of
https://github.com/MeshCore-Beacon/beacon-web.git
synced 2026-10-20 17:51:57 +00:00
Atlas cards now use the Signal tab StatCard tiles, shared Card, badges, action buttons, IATA chips and search bar. Polling moves to TanStack Query with the app retry policy, pauses while rate-limited, and recovers a pin whose server ID changed by its full public key. Search results show a key prefix and explain short or non-hex input; heard-by states which packet it counts. Sparkline component removed in favour of the stats one.
71 lines
4.2 KiB
TypeScript
71 lines
4.2 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { average, signalHours, signalBinLabel, signalHistogramOption, signalTrendOption, signalCoverageOption } from "../../../src/features/stats/signal";
|
|
import { readChartColors } from "../../../src/features/stats/chartTheme";
|
|
import type { SignalStats } from "../../../src/features/stats/types";
|
|
import i18n from "../../../src/i18n";
|
|
|
|
const en = i18n.getFixedT("en");
|
|
const fr = i18n.getFixedT("fr");
|
|
|
|
const hour = 3_600_000;
|
|
export const signalFixture: SignalStats = {
|
|
since: hour / 2, until: 3 * hour + hour / 2, receptions: 100,
|
|
snr: { samples: 80, average: 0, histogram: [{ lower: null, upper: -30, count: 2 }, { lower: -30, upper: -25, count: 78 }, { lower: 30, upper: null, count: 0 }] },
|
|
rssi: { samples: 90, average: -102.5, histogram: [{ lower: -110, upper: -100, count: 90 }] },
|
|
hourly: [
|
|
{ hour: 0, receptions: 50, snrSamples: 50, snrAverage: 0, rssiSamples: 50, rssiAverage: -100 },
|
|
{ hour: 2 * hour, receptions: 40, snrSamples: 30, snrAverage: 0, rssiSamples: 40, rssiAverage: -105.625 },
|
|
{ hour: 3 * hour, receptions: 10, snrSamples: 0, snrAverage: null, rssiSamples: 0, rssiAverage: null },
|
|
],
|
|
};
|
|
|
|
describe("signal analytics", () => {
|
|
it("keeps UTC window edges, true zeros, missing hours and missing readings distinct", () => {
|
|
const before = JSON.stringify(signalFixture);
|
|
const hours = signalHours(signalFixture);
|
|
expect(hours.map((h) => h.hour)).toEqual([0, hour, 2 * hour, 3 * hour]);
|
|
expect(hours.map((h) => h.snrAverage)).toEqual([0, null, 0, null]);
|
|
expect(hours.map((h) => h.receptions)).toEqual([50, null, 40, 10]);
|
|
expect(JSON.stringify(signalFixture)).toBe(before);
|
|
expect(signalHours(undefined)).toEqual([]);
|
|
expect(signalHours({ ...signalFixture, until: 30 * 24 * hour + hour / 2, hourly: [] })).toHaveLength(721);
|
|
});
|
|
it("labels exact half-open bins including overflow without assigning quality ratings", () => {
|
|
expect(signalBinLabel({ lower: null, upper: -30, count: 2 }, en)).toBe("< -30");
|
|
expect(signalBinLabel({ lower: -30, upper: -25, count: 2 }, en)).toBe("-30 to < -25");
|
|
expect(signalBinLabel({ lower: 30, upper: null, count: 2 }, en)).toBe("≥ 30");
|
|
const colors = readChartColors();
|
|
expect(signalHistogramOption(signalFixture.snr, "SNR", "dB", colors, en)).toMatchObject({ animation: false, tooltip: { renderMode: "richText" }, aria: { enabled: true } });
|
|
expect(signalTrendOption(signalHours(signalFixture), "snr", colors, en)).toMatchObject({ animation: false, series: [{ connectNulls: false, showSymbol: true, data: [[0, 0], [hour, null], [2 * hour, 0], [3 * hour, null]] }] });
|
|
});
|
|
|
|
it("translates plotted text without changing bin boundaries, coverage counts or UTC gaps", () => {
|
|
const before = JSON.stringify(signalFixture), colors = readChartColors();
|
|
expect(signalHistogramOption(signalFixture.snr, "SNR", "dB", colors, fr)).toMatchObject({
|
|
tooltip: { renderMode: "richText" },
|
|
aria: { label: { description: expect.stringContaining("Distribution du SNR en dB") } },
|
|
xAxis: { data: ["< -30 dB", "-30 à < -25 dB", "≥ 30 dB"] },
|
|
series: [{ name: "Échantillons SNR", data: [{ value: 2 }, { value: 78 }, { value: 0 }] }],
|
|
});
|
|
expect(signalCoverageOption(signalFixture, colors, fr)).toMatchObject({
|
|
series: [{ name: "Disponibles", data: [80, 90] }, { name: "Indisponibles", data: [20, 10] }],
|
|
});
|
|
const trend = signalTrendOption(signalHours(signalFixture), "snr", colors, fr);
|
|
expect(trend).toMatchObject({ useUTC: true, series: [{ connectNulls: false, data: [[0, 0], [hour, null], [2 * hour, 0], [3 * hour, null]] }] });
|
|
const tooltip = trend.tooltip as { valueFormatter: (value: unknown) => string };
|
|
expect(tooltip.valueFormatter(null)).toBe("Aucun échantillon");
|
|
expect(tooltip.valueFormatter(0)).toBe("0.00 dB");
|
|
expect(JSON.stringify(signalFixture)).toBe(before);
|
|
});
|
|
});
|
|
|
|
describe("average", () => {
|
|
it("formats one decimal with the unit and a dash for no sample", () => {
|
|
expect(average(null)).toBe("—");
|
|
expect(average(undefined, "dB")).toBe("—");
|
|
expect(average(0, "dB")).toBe("0.0 dB");
|
|
expect(average(-102.46, "dBm")).toBe("-102.5 dBm");
|
|
expect(average(2.04)).toBe("2.0");
|
|
});
|
|
});
|