import { describe, it, expect, vi } from "vitest"; import { render, screen, fireEvent, within } from "@testing-library/react"; import { PacketTableRow } from "../../../src/features/packets/PacketTableRow"; import type { LatestObserver, PacketSummary, ResolvedHop } from "../../../src/types/api"; const pkt = (over: Partial = {}): PacketSummary => ({ packetHash: "AA11BB22", payloadType: 1, payloadTypeName: "ADVERT", routeType: 1, routeTypeName: "FLOOD", firstHeardAt: 1700000000, lastHeardAt: 1700000000, observationCount: 3, ...over, }); const node = (name: string): ResolvedHop => ({ confidence: "high", nodes: [{ id: "n-1", name, publicKey: "aabbccdd" }], }); // pathLength is what makes buildPathSummary produce endpoints at all, so it is always present here. const observer = ( over: { hopCount?: number; hashSize?: number } & Partial> = {}, ): LatestObserver => { const { hopCount = 2, hashSize = 1, ...rest } = over; return { id: "abcdef1234", iata: "YVR", pathLength: { raw: "1e", hashSize, hopCount }, ...rest }; }; describe("PacketTableRow", () => { it("exposes one button carrying the expansion state", () => { render( {}} />); const btn = screen.getByRole("button"); expect(btn).toHaveAttribute("aria-expanded", "false"); }); it("toggles on click", () => { const onToggle = vi.fn(); render(); fireEvent.click(screen.getByRole("button")); expect(onToggle).toHaveBeenCalledOnce(); }); it("is a single line, so the row height stays constant for the virtualizer", () => { const { container } = render( {}} />); expect(container.querySelectorAll("button")).toHaveLength(1); expect(screen.queryByText("latest")).not.toBeInTheDocument(); }); it("falls back to n/a in the hops, hash size, endpoint and IATA cells when there is no observer", () => { render( {}} />); const row = within(screen.getByRole("button")); expect(row.getAllByText("n/a")).toHaveLength(4); }); it("shows the hash size alongside the hop count", () => { render( {}} />); expect(screen.getByText("5")).toBeInTheDocument(); expect(screen.getByText("3")).toBeInTheDocument(); }); it("no longer shows the observer, which moved into the expansion", () => { render( {}} />); expect(screen.queryByText("Cypress Peak")).not.toBeInTheDocument(); expect(screen.queryByText("abcdef12")).not.toBeInTheDocument(); expect(screen.getByText("YVR")).toBeInTheDocument(); }); it("shows the hop count, which the REST list carries on every row", () => { render( {}} />); expect(screen.getByText("3")).toBeInTheDocument(); }); it("renders resolved endpoints when the WS feed supplied them", () => { render( {}} />, ); expect(screen.getByText("Laprairie")).toBeInTheDocument(); expect(screen.getByText("YUL1")).toBeInTheDocument(); }); // The REST list leaves resolvedSource/Destination nil on purpose, so scrollback rows show one n/a // for the pair rather than "n/a → n/a". it("collapses the endpoint cell to a single n/a when neither endpoint resolved", () => { render( {}} />); expect(screen.getAllByText("n/a")).toHaveLength(1); }); it("still marks the missing half when only one endpoint resolved", () => { render( {}} />); expect(screen.getByText("Laprairie")).toBeInTheDocument(); expect(screen.getAllByText("n/a")).toHaveLength(1); }); it("reflects the expanded state on the button and chevron", () => { render( {}} />); expect(screen.getByRole("button")).toHaveAttribute("aria-expanded", "true"); expect(screen.getByText("›")).toHaveClass("rotate-90"); }); it("marks a fresh row with the pulse class", () => { const { container } = render( {}} />); expect(container.querySelector(".packet-fresh")).toBeInTheDocument(); }); it("renders a scope tag when the packet has a scope", () => { render( {}} />); expect(screen.getByText("#bc")).toBeInTheDocument(); }); it("falls back to the raw payload type name for an unrecognized payload type", () => { render( {}} />); expect(screen.getByText("CUSTOM_99")).toBeInTheDocument(); }); it("falls back to Unknown when routeTypeName is empty", () => { render( {}} />); expect(screen.getByText("Unknown")).toBeInTheDocument(); }); });