import { describe, expect, it, vi } from "vitest"; import { fireEvent, render, screen, within } from "@testing-library/react"; import { ObserverSummary } from "../../../src/features/observers/ObserverSummary"; import type { Observer } from "../../../src/features/observers/types"; import type { ObserverActivity, TelemetryPoint } from "../../../src/features/stats/types"; const observer = { id: "observer", displayName: "Roof", iata: "YKF", brokers: [], lastStatusAt: Date.now() - 600_000 } as unknown as Observer; describe("compact observer header", () => { it("uses an accessible stale icon and omits the traffic text badge", () => { render(); expect(screen.getByRole("img", { name: "Status is stale" })).toHaveClass("text-warn"); expect(screen.queryByText("Status is stale")).not.toBeInTheDocument(); expect(screen.queryByText(/No recent recorded packets|Packet freshness unavailable/)).not.toBeInTheDocument(); }); it("distinguishes recent and absent status without inventing telemetry", () => { const { rerender } = render(); expect(screen.getByRole("img", { name: "Recent status" })).toHaveClass("text-green"); rerender(); expect(screen.getByRole("img", { name: "No status report" })).toHaveClass("text-text-muted"); }); }); describe("observer summary card notes", () => { it("leaves unread activity gaps in both packet sparklines", () => { const hour = 3_600_000; const activity = { interval: "1h", windowStart: 0, windowEnd: 5 * hour, rolledUntil: 2 * hour, rawFrom: 4 * hour, points: [0, 1, 4, 5].map(h => ({ t: h * hour, observations: h + 1, packets: h + 1 })), summary: { recordedPackets: 14, lastCompleteHour: 6, lastCompleteHourEnd: 6 * hour }, } as unknown as ObserverActivity; render(); for (const label of ["Packets heard", "Packets last hour"]) { expect(screen.getByText(label).closest("li")!.querySelectorAll("polyline")).toHaveLength(2); } }); it("keeps the status icon and IATA chip on the name row", () => { render(); const row = screen.getByRole("heading", { level: 1 }).parentElement!; expect(screen.getByRole("img", { name: "Status is stale" }).parentElement).toBe(row); expect(screen.getByText("YKF").parentElement).toBe(row); }); it("labels the last complete hour plainly, without a UTC window", () => { const activity = { summary: { recordedPackets: 5, lastCompleteHour: 3, lastCompleteHourStart: 0, lastCompleteHourEnd: 3_600_000, latestRecordedAt: null, } } as unknown as ObserverActivity; render(); const tile = screen.getByText("Packets last hour").closest("li")!; expect(tile).toHaveTextContent("3"); expect(tile).toHaveClass("text-center"); expect(screen.queryByText(/UTC/)).not.toBeInTheDocument(); }); it("labels the noise tile as the last reading and sets the unit apart from the number", () => { const points = [{ noiseFloorDb: -91 } as unknown as TelemetryPoint]; render(); const tile = screen.getByText("Noise floor").closest("li")!; expect(screen.queryByText(/Latest telemetry interval|Latest status/)).not.toBeInTheDocument(); expect(within(tile).getByText("-91")).not.toHaveClass("text-sm"); expect(within(tile).getByText("dBm")).toHaveClass("text-sm"); }); }); describe("observer identity panel", () => { const full = { ...observer, hardwareModel: "Heltec V3", firmwareVersion: "v1.16.0-observer-8fa3870", softwareVersion: "meshcore/v1.16.0-observer-8fa3870", radioFreqMhz: 910.525, radioSf: 7, radioBwKhz: 62.5, radioCr: 5, publicKey: "04ebc137a3e94d78c376ee356293343f1751f3d9e023960af02c14c63b839a29", firstSeen: Date.now() - 86_400_000, brokers: [{ name: "mqtt1", lastSeenAt: Date.now() - 60_000, lastPacketAt: Date.now() - 60_000 }], } as unknown as Observer; it("puts device details beside the name instead of a separate card", () => { render(); const panel = screen.getByRole("region", { name: "Roof" }); expect(panel).toContainElement(screen.getByRole("heading", { level: 1, name: "Roof" })); expect(panel).toHaveTextContent("Heltec V3"); expect(panel).toHaveTextContent("v1.16.0-observer-8fa3870"); expect(panel).toHaveTextContent("910.525 MHz"); expect(panel).not.toHaveTextContent("meshcore/v1.16.0"); expect(panel).toHaveTextContent("mqtt1"); expect(panel).toHaveTextContent("1d ago"); }); it("shortens the public key but copies all of it", () => { const writeText = vi.fn(); Object.assign(navigator, { clipboard: { writeText } }); render(); expect(screen.getByText("04ebc137…b839a29")).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "Copy public key" })); expect(writeText).toHaveBeenCalledWith(full.publicKey); }); it("stacks observer actions in their own column of the header panel", () => { render(} />); const panel = screen.getByRole("region", { name: "Roof" }); const column = within(panel).getByRole("button", { name: "Compare with…" }).parentElement!; expect(column).toContainElement(within(panel).getByRole("button", { name: "Copy link" })); expect(column).not.toContainElement(screen.getByRole("heading", { level: 1 })); expect(column).toHaveClass("sm:flex-col"); }); it("labels each detail", () => { render(); const panel = screen.getByRole("region", { name: "Roof" }); const term = (label: string) => within(panel).getByText(label, { selector: "dt" }).nextElementSibling!; expect(term("Device")).toHaveTextContent("Heltec V3"); expect(term("Public key")).toHaveTextContent("04ebc137…b839a29"); expect(term("Brokers")).toHaveTextContent("mqtt1"); expect(term("First seen")).toHaveTextContent("1d ago"); }); it("keeps a client version that adds information", () => { render(); expect(screen.getByRole("region", { name: "Roof" })).toHaveTextContent("meshcore-mqtt 2.1"); }); });