import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { render, screen, fireEvent } from "@testing-library/react"; import { MemoryRouter, useLocation } from "react-router-dom"; import { PacketAnalyzerDrawer } from "../../../src/features/packets/PacketAnalyzerDrawer"; import type { PacketDetail } from "../../../src/types/api"; import { PayloadType, RouteType } from "../../../src/types/enums"; function LocationProbe() { const location = useLocation(); return
{location.search}
; } describe("PacketAnalyzerDrawer close", () => { it("removes ?analyze but keeps ?hash, and calls onClose", () => { const onClose = vi.fn(); render( , ); fireEvent.click(screen.getByLabelText("Close analyzer")); expect(onClose).toHaveBeenCalledOnce(); const search = screen.getByTestId("search").textContent ?? ""; expect(search).not.toContain("analyze="); expect(search).toContain("hash=abc123"); // row stays expanded expect(search).toContain("tab=Packets"); // other params survive }); }); const hop = (id: string, lng: number, lat: number) => ({ confidence: "high" as const, nodes: [{ id, publicKey: "pk", longitude: lng, latitude: lat }] }); function makeDetail(resolvedPath: unknown[]): PacketDetail { return { packetHash: "abcdef12", header: { raw: "12", routeType: RouteType.FLOOD, routeTypeName: "FLOOD", payloadType: PayloadType.TEXT, payloadTypeName: "TXT_MSG", payloadVersion: 1 }, firstHeardAt: 0, lastHeardAt: 0, firstToLastMs: 0, observationCount: 1, rawPayload: "", decrypted: false, observations: [{ id: 1, observerId: "obs12345", iata: "YYZ", heardAt: 0, sourceBroker: "b", pathLength: { raw: "02", hashSize: 1, hopCount: resolvedPath.length }, resolvedPath }], } as unknown as PacketDetail; } describe("PacketAnalyzerDrawer copy link", () => { const writeText = vi.fn(); beforeEach(() => { Object.defineProperty(navigator, "clipboard", { value: { writeText }, writable: true, configurable: true }); writeText.mockClear(); }); afterEach(() => window.history.replaceState({}, "", "/")); it("copies a link that reopens the drawer over the expanded row", () => { render( {}} /> , ); fireEvent.click(screen.getByRole("button", { name: "Copy packet link" })); const copied = new URL(writeText.mock.calls[0]![0] as string); expect(copied.searchParams.get("tab")).toBe("Packets"); expect(copied.searchParams.get("hash")).toBe("abcdef12"); expect(copied.searchParams.get("analyze")).toBe("1"); // the drawer is part of the shared state }); }); describe("PacketAnalyzerDrawer view-path button", () => { it("enables the button and calls onViewPath when a path is drawable", () => { const onViewPath = vi.fn(); render( {}} onViewPath={onViewPath} /> , ); const btn = screen.getByRole("button", { name: /view path on map/i }); expect(btn).toBeEnabled(); fireEvent.click(btn); expect(onViewPath).toHaveBeenCalledOnce(); }); it("disables the button when no path is drawable", () => { render( {}} onViewPath={() => {}} /> , ); expect(screen.getByRole("button", { name: /view path on map/i })).toBeDisabled(); }); }); describe("PacketAnalyzerDrawer TRACE path data", () => { // After beacon-server's trace-path fix (7a58a07) a TRACE observation's pathBytes are the trace's // own path hashes (with matching hashSize/hopCount and a real resolvedPath), not raw SNR bytes — // so it must render as resolved Path Data, not under the old "Path SNR Data" label. function traceDetail(): PacketDetail { return { packetHash: "abcdef12", header: { raw: "12", routeType: RouteType.FLOOD, routeTypeName: "FLOOD", payloadType: PayloadType.TRACE, payloadTypeName: "TRACE", payloadVersion: 1 }, firstHeardAt: 0, lastHeardAt: 0, firstToLastMs: 0, observationCount: 1, rawPayload: "", decrypted: false, observations: [{ id: 1, observerId: "obs12345", iata: "YYZ", heardAt: 0, sourceBroker: "b", pathLength: { raw: "02", hashSize: 1, hopCount: 2 }, pathBytes: "abcd", resolvedPath: [hop("a", -79, 43), hop("b", -75, 45)], }], } as unknown as PacketDetail; } it("renders TRACE path bytes as resolved Path Data, not raw 'Path SNR Data'", () => { render( {}} /> , ); expect(screen.queryByText("Path SNR Data")).not.toBeInTheDocument(); expect(screen.getByText("Path Data")).toBeInTheDocument(); // the first trace hash renders as a resolved hop block, tinted green for high confidence expect(screen.getAllByText("AB").some((el) => el.className.includes("text-green"))).toBe(true); }); });