mirror of
https://github.com/MeshCore-Beacon/beacon-web.git
synced 2026-09-02 09:03:44 +00:00
* stats: node-types donut, multi-IATA filtering, preset split - node-types: new /stats/node-types endpoint + useNodeTypes; donutOption replaces the "Coming soon" card with a live donut - stats endpoints take iatas[] instead of a single iata, so multi-IATA regions filter instead of falling back to all (drops useStatsIata) - radio presets keep the node/observer split via stacked presetBarsOption - drop client-side telemetry ms-normalization (backend now emits epoch ms on both paths); charts pick delta-vs-raw counters off the response interval - MeshTab: range-driven charts lead, all-time charts follow; KPI window from overview.windowHours instead of a hardcoded 24h - ci: docker-publish builds a :dev image on dev-branch pushes * feat: traces vs pings feat: SNR for trace path in list feat: known niehgbour count * fix: github workflow, post images publically * build(deps): bump undici from 7.27.2 to 7.28.0 (#9) Bumps [undici](https://github.com/nodejs/undici) from 7.27.2 to 7.28.0. - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v7.27.2...v7.28.0) --- updated-dependencies: - dependency-name: undici dependency-version: 7.28.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat: discover renderers, neighbor map lines, live packet flow - Discover packets decoded: DISCOVER_REQ/DISCOVER_RESP render real fields (type-filter roles, tag, node-to-node request SNR) instead of a raw dump. - Neighbor lines on the map: On/Selected/Off control draws links between nodes and their known neighbors. - Live packet flow: a play button animates packets hop-to-hop between repeaters in real time (off by default). * map: live mode dims nodes, flashes each packet's route * map: shoot a comet along each packet's route in live mode * map: rework live mode — packet dot, dashed trail, per-hop flash * map: fade live-mode clusters more so dense ones stay see-through * map: persist clustering + node-type, default neighbor lines to selected * map: fade live-mode node markers more so overlaps stay see-through * map: fade the live-mode node flash in sync with its packet trail * nodes: let the mobile detail panel minimize without deselecting * map: dim other nodes to spotlight a selected node and its neighbours * map: colour a selected node's neighbour lines by obs count and freshness * Add a neighbour graph under the renamed Analytics tab * Focus the neighbour graph into an ego view on click * Wrap the analytics sub-header so the range stays on-screen on mobile * Use a dropdown for the analytics sections on mobile * Gate the neighbour graph to a region and size labels by busyness * Add a name search that spotlights matches and dims the rest of the mesh * Add a copy button beside node and observer public keys * Add a copy link to node and observer detail panels Deep-linked ?node/?observer selections now survive load: the region reset watches the raw selection instead of the async-resolved regionKey, so slug expansion no longer wipes a restored panel. * Add .env options for hiding tabs, MeshMapper themes, app name and a GitHub link * Make VITE_ENABLED_THEMES an exclusive theme allowlist * Add a .env option to skip the load splash * Keep the packets list static while scrolled down instead of jumping on new packets * Add Map deep-links for camera and settings with a copy-link button * Filter packet history on the server when a single type, route or scope is selected * Show loading feedback while packet history loads * build(deps): bump echarts from 5.5.1 to 6.1.0 (#14) * Fix: github workflow + stats tab + add traces vs pings (#10) * stats: node-types donut, multi-IATA filtering, preset split - node-types: new /stats/node-types endpoint + useNodeTypes; donutOption replaces the "Coming soon" card with a live donut - stats endpoints take iatas[] instead of a single iata, so multi-IATA regions filter instead of falling back to all (drops useStatsIata) - radio presets keep the node/observer split via stacked presetBarsOption - drop client-side telemetry ms-normalization (backend now emits epoch ms on both paths); charts pick delta-vs-raw counters off the response interval - MeshTab: range-driven charts lead, all-time charts follow; KPI window from overview.windowHours instead of a hardcoded 24h - ci: docker-publish builds a :dev image on dev-branch pushes * feat: traces vs pings feat: SNR for trace path in list feat: known niehgbour count * fix: github workflow, post images publically * build(deps): bump echarts from 5.5.1 to 6.1.0 Bumps [echarts](https://github.com/apache/echarts) from 5.5.1 to 6.1.0. - [Release notes](https://github.com/apache/echarts/releases) - [Commits](https://github.com/apache/echarts/compare/5.5.1...6.1.0) --- updated-dependencies: - dependency-name: echarts dependency-version: 6.1.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump version to 1.2.0 --------- Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
81 lines
3.7 KiB
TypeScript
81 lines
3.7 KiB
TypeScript
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
import { renderHook, waitFor } from "@testing-library/react";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import type { ReactNode } from "react";
|
|
import { useMapNodesData } from "../../../src/features/map/useMapNodesData";
|
|
import { getNodesPage } from "../../../src/api/client";
|
|
import type { NodeSummary } from "../../../src/features/nodes/types";
|
|
|
|
vi.mock("../../../src/api/client", () => ({ getNodesPage: vi.fn() }));
|
|
|
|
const mockGetNodesPage = vi.mocked(getNodesPage);
|
|
|
|
function node(id: string): NodeSummary {
|
|
return { id, publicKey: id, nodeType: 1, nodeTypeName: "repeater", name: id, lat: 0, lng: 0, iatas: [] };
|
|
}
|
|
|
|
function wrapper() {
|
|
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
return ({ children }: { children: ReactNode }) => (
|
|
<QueryClientProvider client={client}>{children}</QueryClientProvider>
|
|
);
|
|
}
|
|
|
|
beforeEach(() => {
|
|
mockGetNodesPage.mockReset();
|
|
});
|
|
|
|
describe("useMapNodesData", () => {
|
|
it("auto-chains pages and concatenates every page's nodes", async () => {
|
|
mockGetNodesPage
|
|
.mockResolvedValueOnce({ items: [node("a"), node("b")], nextCursor: 2, hasMore: true })
|
|
.mockResolvedValueOnce({ items: [node("c")], nextCursor: null, hasMore: false });
|
|
|
|
const { result } = renderHook(() => useMapNodesData(["YYZ"], "YYZ"), { wrapper: wrapper() });
|
|
|
|
await waitFor(() => expect(result.current.isPaging).toBe(false));
|
|
|
|
expect(result.current.nodes.map((n) => n.id)).toEqual(["a", "b", "c"]);
|
|
expect(result.current.loadedCount).toBe(3);
|
|
expect(mockGetNodesPage).toHaveBeenCalledTimes(2);
|
|
// second call paginates with the previous page's nextCursor; the map always requests neighbor ids
|
|
expect(mockGetNodesPage).toHaveBeenLastCalledWith(["YYZ"], { cursor: 2, neighbors: true });
|
|
});
|
|
|
|
it("stops after a single page when hasMore is false", async () => {
|
|
mockGetNodesPage.mockResolvedValueOnce({ items: [node("a")], nextCursor: null, hasMore: false });
|
|
|
|
const { result } = renderHook(() => useMapNodesData(undefined, "*"), { wrapper: wrapper() });
|
|
|
|
await waitFor(() => expect(result.current.isPaging).toBe(false));
|
|
expect(result.current.loadedCount).toBe(1);
|
|
expect(mockGetNodesPage).toHaveBeenCalledTimes(1); // no over-fetch past the last page
|
|
});
|
|
|
|
it("stops chaining (no retry loop) when a page fetch fails", async () => {
|
|
mockGetNodesPage
|
|
.mockResolvedValueOnce({ items: [node("a")], nextCursor: 1, hasMore: true })
|
|
.mockRejectedValue(new Error("boom"));
|
|
|
|
const { result } = renderHook(() => useMapNodesData(["YYZ"], "YYZ"), { wrapper: wrapper() });
|
|
|
|
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
// the failed page must not re-arm the auto-chain effect: exactly page 1 + one failed page 2
|
|
expect(mockGetNodesPage).toHaveBeenCalledTimes(2);
|
|
expect(result.current.isPaging).toBe(false); // pill hides on error instead of hanging on
|
|
expect(result.current.nodes.map((n) => n.id)).toEqual(["a"]); // page 1 still shows
|
|
});
|
|
|
|
it("dedupes nodes that repeat across a page boundary (cursor is a non-unique timestamp)", async () => {
|
|
mockGetNodesPage
|
|
.mockResolvedValueOnce({ items: [node("a"), node("b")], nextCursor: 5, hasMore: true })
|
|
.mockResolvedValueOnce({ items: [node("b"), node("c")], nextCursor: null, hasMore: false });
|
|
|
|
const { result } = renderHook(() => useMapNodesData(["YYZ"], "YYZ"), { wrapper: wrapper() });
|
|
|
|
await waitFor(() => expect(result.current.isPaging).toBe(false));
|
|
expect(result.current.nodes.map((n) => n.id)).toEqual(["a", "b", "c"]); // b not doubled
|
|
expect(result.current.loadedCount).toBe(3);
|
|
});
|
|
});
|