mirror of
https://github.com/MeshCore-Beacon/beacon-web.git
synced 2026-10-23 14:32:28 +00:00
Bugs: clear compare params on tab change, recover channel live messages after a 429, observer copy link keeps only region params, route table no longer navigates on region change with nothing selected, map borders memo on data identity, live overview can't resurrect a failed query, French relative times, no language write on first visit, 0/0 node locations hidden outside the map, path/signal stats use the shared retry rule. i18n: node/packet/mesh/observer strings that were still hardcoded after #62. Cleanup: formatUtc, ACTION_BUTTON_CLASS, observer-stats, useWindowedStats, observer-id; observer pages use StatCard/Card/Field/Segmented/SearchBar/SelectDropdown; one details chrome on the stats tabs; comment and translations-guide trims; localStorage stub for Node 26 in the test setup.
113 lines
4.6 KiB
TypeScript
113 lines
4.6 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import { renderHook } from "@testing-library/react";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import type { ReactNode } from "react";
|
|
import { useLiveOverview, useLiveObserver } from "../../../src/features/stats/useLiveStats";
|
|
import type { StatsOverview } from "../../../src/features/stats/types";
|
|
import type { WsManager } from "../../../src/api/ws-manager";
|
|
import type { WsObserverStatus, WsPacketObservation } from "../../../src/types/ws";
|
|
|
|
vi.mock("../../../src/hooks/useRegion", () => ({
|
|
useRegion: () => ({ iatas: ["YOW"], regionKey: "YOW" }),
|
|
}));
|
|
|
|
const overview: StatsOverview = {
|
|
totalPackets: 100,
|
|
totalObservations: 200,
|
|
activeObservers: 5,
|
|
activeIatas: 2,
|
|
windowHours: 24,
|
|
};
|
|
|
|
// Captures the packet handler the hook registers and lets the test push events through it.
|
|
function fakeManager() {
|
|
const handlers: Array<(d: WsPacketObservation["data"]) => void> = [];
|
|
const manager = {
|
|
onPacketObservation: (h: (d: WsPacketObservation["data"]) => void) => {
|
|
handlers.push(h);
|
|
return () => {};
|
|
},
|
|
} as unknown as WsManager;
|
|
return { manager, emit: (d: WsPacketObservation["data"]) => handlers.forEach((h) => h(d)) };
|
|
}
|
|
|
|
describe("useLiveOverview", () => {
|
|
let rafCallbacks: FrameRequestCallback[];
|
|
|
|
beforeEach(() => {
|
|
rafCallbacks = [];
|
|
vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => {
|
|
rafCallbacks.push(cb);
|
|
return rafCallbacks.length;
|
|
});
|
|
vi.stubGlobal("cancelAnimationFrame", () => {});
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it("bumps the cached stats-overview counters on packet observations", () => {
|
|
const qc = new QueryClient();
|
|
qc.setQueryData<StatsOverview>(["stats-overview", "YOW"], overview);
|
|
const { manager, emit } = fakeManager();
|
|
|
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
<QueryClientProvider client={qc}>{children}</QueryClientProvider>
|
|
);
|
|
renderHook(() => useLiveOverview(manager), { wrapper });
|
|
|
|
emit({ packet: { isFirstObservation: true } } as WsPacketObservation["data"]);
|
|
emit({ packet: { isFirstObservation: false } } as WsPacketObservation["data"]);
|
|
rafCallbacks.forEach((cb) => cb(0)); // flush the coalesced frame
|
|
|
|
const after = qc.getQueryData<StatsOverview>(["stats-overview", "YOW"]);
|
|
expect(after?.totalPackets).toBe(101);
|
|
expect(after?.totalObservations).toBe(202);
|
|
});
|
|
|
|
it("drops pending deltas instead of resurrecting a failed overview query", () => {
|
|
const qc = new QueryClient();
|
|
qc.setQueryData<StatsOverview>(["stats-overview", "YOW"], overview);
|
|
qc.getQueryCache().find({ queryKey: ["stats-overview", "YOW"] })?.setState({ status: "error", error: new Error("refetch failed") });
|
|
const { manager, emit } = fakeManager();
|
|
|
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
<QueryClientProvider client={qc}>{children}</QueryClientProvider>
|
|
);
|
|
renderHook(() => useLiveOverview(manager), { wrapper });
|
|
|
|
emit({ packet: { isFirstObservation: true } } as WsPacketObservation["data"]);
|
|
rafCallbacks.forEach((cb) => cb(0));
|
|
|
|
const state = qc.getQueryState<StatsOverview>(["stats-overview", "YOW"]);
|
|
expect(state?.status).toBe("error");
|
|
expect(state?.data).toEqual(overview);
|
|
});
|
|
});
|
|
|
|
describe("useLiveObserver", () => {
|
|
it("refreshes the selected observer's telemetry, but not its heard activity, on a status event", () => {
|
|
const qc = new QueryClient();
|
|
qc.setQueryData(["observer-telemetry", "obs-1", "24h", "1h"], { range: "24h", interval: "1h", points: [] });
|
|
qc.setQueryData(["observer-activity", "obs-1", "24h"], { range: "24h", interval: "15m", radio: null, payloadTypes: [], points: [] });
|
|
const handlers: Array<(d: WsObserverStatus["data"]) => void> = [];
|
|
const manager = {
|
|
onObserverStatus: (h: (d: WsObserverStatus["data"]) => void) => {
|
|
handlers.push(h);
|
|
return () => {};
|
|
},
|
|
} as unknown as WsManager;
|
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
<QueryClientProvider client={qc}>{children}</QueryClientProvider>
|
|
);
|
|
renderHook(() => useLiveObserver(manager, "obs-1", "24h"), { wrapper });
|
|
|
|
handlers.forEach((h) => h({ observerId: "obs-1" } as WsObserverStatus["data"]));
|
|
|
|
expect(qc.getQueryState(["observer-telemetry", "obs-1", "24h", "1h"])?.isInvalidated).toBe(true);
|
|
// heard activity changes with packets, not status, and refreshes on its own interval
|
|
expect(qc.getQueryState(["observer-activity", "obs-1", "24h"])?.isInvalidated).toBe(false);
|
|
});
|
|
});
|