Files
MrAlders0n b427282b30 Rebuild My Atlas on the shared Analytics cards (2.0.5)
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.
2026-10-12 13:18:24 -04:00

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");
});
});