mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-04-25 15:22:10 +00:00
216 lines
9.1 KiB
JavaScript
216 lines
9.1 KiB
JavaScript
import { describe, it, expect } from "vitest";
|
|
import { extentDiagonal, buildClusterItems, getFeatureCoord } from "@/components/map/internal/clusterUtils.js";
|
|
import { getDiscoveredIconName } from "@/components/map/internal/discoveredIcons.js";
|
|
import { dedupeDiscoveredMapNodes, dedupeTelemetryMarkersForMap } from "@/components/map/internal/mapDedupe.js";
|
|
|
|
function feature(props, coord) {
|
|
const geom = coord ? { getCoordinates: () => coord } : null;
|
|
return {
|
|
get: (key) => props[key],
|
|
set: (key, value) => {
|
|
props[key] = value;
|
|
},
|
|
getGeometry: () => geom,
|
|
};
|
|
}
|
|
|
|
describe("clusterUtils.extentDiagonal", () => {
|
|
it("returns 0 for invalid extents", () => {
|
|
expect(extentDiagonal(null)).toBe(0);
|
|
expect(extentDiagonal(undefined)).toBe(0);
|
|
expect(extentDiagonal([])).toBe(0);
|
|
expect(extentDiagonal([1, 2, 3])).toBe(0);
|
|
expect(extentDiagonal([Infinity, Infinity, -Infinity, -Infinity])).toBe(0);
|
|
});
|
|
|
|
it("returns the diagonal length for valid extents", () => {
|
|
expect(extentDiagonal([0, 0, 3, 4])).toBeCloseTo(5);
|
|
expect(extentDiagonal([10, 10, 10, 10])).toBe(0);
|
|
expect(extentDiagonal([-2, -2, 2, 2])).toBeCloseTo(Math.sqrt(32));
|
|
});
|
|
});
|
|
|
|
describe("clusterUtils.getFeatureCoord", () => {
|
|
it("returns the originalCoord when set", () => {
|
|
const f = feature({ originalCoord: [11, 22] }, [99, 99]);
|
|
expect(getFeatureCoord(f)).toEqual([11, 22]);
|
|
});
|
|
|
|
it("falls back to the geometry's coordinates", () => {
|
|
const f = feature({}, [33, 44]);
|
|
expect(getFeatureCoord(f)).toEqual([33, 44]);
|
|
});
|
|
|
|
it("returns null for missing feature or geometry", () => {
|
|
expect(getFeatureCoord(null)).toBeNull();
|
|
expect(getFeatureCoord(feature({}, null))).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("clusterUtils.buildClusterItems", () => {
|
|
it("returns an empty array for missing features or empty clusters", () => {
|
|
expect(buildClusterItems(null)).toEqual([]);
|
|
expect(buildClusterItems(feature({ clusterItems: [] }, [0, 0]))).toEqual([]);
|
|
});
|
|
|
|
it("normalises telemetry, discovered, and unknown items", () => {
|
|
const items = [
|
|
feature(
|
|
{
|
|
telemetry: { destination_hash: "deadbeefcafe1234" },
|
|
peer: { display_name: "Alice" },
|
|
originalCoord: [10, 20],
|
|
},
|
|
[10, 20]
|
|
),
|
|
feature(
|
|
{
|
|
discovered: { name: "RNode", interface: "AutoIf", type: "AutoInterface" },
|
|
originalCoord: [11, 21],
|
|
},
|
|
[11, 21]
|
|
),
|
|
feature({ originalCoord: [12, 22] }, [12, 22]),
|
|
];
|
|
const cluster = feature({ clusterItems: items }, [11, 21]);
|
|
const out = buildClusterItems(cluster);
|
|
expect(out).toHaveLength(3);
|
|
expect(out[0]).toMatchObject({ kind: "telemetry", label: "Alice", identifier: "deadbeefcafe1234" });
|
|
expect(out[1]).toMatchObject({ kind: "discovered", label: "RNode", identifier: "AutoIf" });
|
|
expect(out[2]).toMatchObject({ kind: "unknown", label: "Unknown" });
|
|
});
|
|
|
|
it("falls back to a truncated destination hash when no peer name is available", () => {
|
|
const items = [feature({ telemetry: { destination_hash: "abcdef0123456789" } }, [0, 0])];
|
|
const cluster = feature({ clusterItems: items }, [0, 0]);
|
|
expect(buildClusterItems(cluster)[0].label).toBe("abcdef01");
|
|
});
|
|
|
|
it("ignores nullish entries inside the cluster", () => {
|
|
const cluster = feature(
|
|
{ clusterItems: [null, feature({ telemetry: { destination_hash: "x" } }, [0, 0])] },
|
|
[0, 0]
|
|
);
|
|
expect(buildClusterItems(cluster)).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe("discoveredIcons.getDiscoveredIconName", () => {
|
|
it("falls back to a generic icon for null/empty input", () => {
|
|
expect(getDiscoveredIconName(null)).toBe("map-marker-radius");
|
|
expect(getDiscoveredIconName(undefined)).toBe("map-marker-radius");
|
|
expect(getDiscoveredIconName({})).toBe("server-network");
|
|
});
|
|
|
|
it("maps known interface types to their MDI names", () => {
|
|
expect(getDiscoveredIconName({ type: "AutoInterface" })).toBe("home-automation");
|
|
expect(getDiscoveredIconName({ type: "RNodeMultiInterface" })).toBe("access-point-network");
|
|
expect(getDiscoveredIconName({ type: "TCPClientInterface" })).toBe("lan-connect");
|
|
expect(getDiscoveredIconName({ type: "BackboneInterface" })).toBe("lan-connect");
|
|
expect(getDiscoveredIconName({ type: "TCPServerInterface" })).toBe("lan");
|
|
expect(getDiscoveredIconName({ type: "UDPInterface" })).toBe("wan");
|
|
expect(getDiscoveredIconName({ type: "SerialInterface" })).toBe("usb-port");
|
|
expect(getDiscoveredIconName({ type: "KISSInterface" })).toBe("antenna");
|
|
expect(getDiscoveredIconName({ type: "AX25KISSInterface" })).toBe("antenna");
|
|
expect(getDiscoveredIconName({ type: "I2PInterface" })).toBe("eye");
|
|
expect(getDiscoveredIconName({ type: "PipeInterface" })).toBe("pipe");
|
|
});
|
|
|
|
it("differentiates serial vs TCP for RNodeInterface based on port", () => {
|
|
expect(getDiscoveredIconName({ type: "RNodeInterface" })).toBe("radio-tower");
|
|
expect(getDiscoveredIconName({ type: "RNodeInterface", port: "/dev/ttyUSB0" })).toBe("radio-tower");
|
|
expect(getDiscoveredIconName({ type: "RNodeInterface", port: "tcp://10.0.0.1:4242" })).toBe("lan-connect");
|
|
});
|
|
|
|
it("uses interface_type as a fallback when type is missing", () => {
|
|
expect(getDiscoveredIconName({ interface_type: "AutoInterface" })).toBe("home-automation");
|
|
});
|
|
});
|
|
|
|
describe("mapDedupe.dedupeDiscoveredMapNodes", () => {
|
|
it("returns an empty array for invalid input", () => {
|
|
expect(dedupeDiscoveredMapNodes(null)).toEqual([]);
|
|
expect(dedupeDiscoveredMapNodes(undefined)).toEqual([]);
|
|
expect(dedupeDiscoveredMapNodes([])).toEqual([]);
|
|
});
|
|
|
|
it("preserves nameless entries even at the same location", () => {
|
|
const nodes = [
|
|
{ name: "", latitude: 1, longitude: 1, last_heard: 100 },
|
|
{ name: "", latitude: 1, longitude: 1, last_heard: 200 },
|
|
];
|
|
expect(dedupeDiscoveredMapNodes(nodes)).toHaveLength(2);
|
|
});
|
|
|
|
it("collapses same-name nearby duplicates and keeps the freshest", () => {
|
|
const nodes = [
|
|
{ name: "Alpha", latitude: 1, longitude: 1, last_heard: 100 },
|
|
{ name: "alpha", latitude: 1.001, longitude: 1.001, last_heard: 500 },
|
|
{ name: "Beta", latitude: 5, longitude: 5, last_heard: 300 },
|
|
];
|
|
const out = dedupeDiscoveredMapNodes(nodes);
|
|
expect(out).toHaveLength(2);
|
|
const alpha = out.find((n) => n.name.toLowerCase() === "alpha");
|
|
expect(alpha.last_heard).toBe(500);
|
|
});
|
|
|
|
it("keeps same-name nodes that are far apart", () => {
|
|
const nodes = [
|
|
{ name: "Hub", latitude: 0, longitude: 0, last_heard: 100 },
|
|
{ name: "Hub", latitude: 10, longitude: 10, last_heard: 200 },
|
|
];
|
|
expect(dedupeDiscoveredMapNodes(nodes)).toHaveLength(2);
|
|
});
|
|
});
|
|
|
|
describe("mapDedupe.dedupeTelemetryMarkersForMap", () => {
|
|
const peers = {
|
|
AAA: { display_name: "Alice" },
|
|
BBB: { display_name: "Alice" },
|
|
CCC: { display_name: "Bob" },
|
|
};
|
|
|
|
function entry(hash, lat, lon, ts) {
|
|
return {
|
|
destination_hash: hash,
|
|
updated_at: new Date(ts * 1000).toISOString(),
|
|
telemetry: { location: { latitude: lat, longitude: lon } },
|
|
};
|
|
}
|
|
|
|
it("returns an empty array for invalid input", () => {
|
|
expect(dedupeTelemetryMarkersForMap(null)).toEqual([]);
|
|
expect(dedupeTelemetryMarkersForMap(undefined)).toEqual([]);
|
|
expect(dedupeTelemetryMarkersForMap([])).toEqual([]);
|
|
});
|
|
|
|
it("collapses peers with the same display name in the same place", () => {
|
|
const list = [entry("AAA", 1, 1, 100), entry("BBB", 1.001, 1.001, 200), entry("CCC", 1, 1, 50)];
|
|
const out = dedupeTelemetryMarkersForMap(list, peers);
|
|
expect(out).toHaveLength(2);
|
|
expect(out[0].destination_hash).toBe("BBB");
|
|
});
|
|
|
|
it("keeps same-name peers that are far apart", () => {
|
|
const list = [entry("AAA", 0, 0, 100), entry("BBB", 10, 10, 200)];
|
|
expect(dedupeTelemetryMarkersForMap(list, peers)).toHaveLength(2);
|
|
});
|
|
|
|
it("falls back to a truncated hash when no peer name is registered", () => {
|
|
const list = [entry("ffffff0011", 0, 0, 100), entry("ffffff0011", 0.001, 0.001, 200)];
|
|
const out = dedupeTelemetryMarkersForMap(list, {});
|
|
expect(out).toHaveLength(1);
|
|
expect(out[0].updated_at).toBe(list[1].updated_at);
|
|
});
|
|
|
|
it("preserves entries whose location is missing from comparison", () => {
|
|
const list = [entry("AAA", 0, 0, 100), { destination_hash: "AAA", updated_at: list1Time(200) }];
|
|
const out = dedupeTelemetryMarkersForMap(list, peers);
|
|
expect(out).toHaveLength(2);
|
|
});
|
|
});
|
|
|
|
function list1Time(ts) {
|
|
return new Date(ts * 1000).toISOString();
|
|
}
|