From 8795c360ff70ebba3c021b9f8f5474d9a46bd451 Mon Sep 17 00:00:00 2001 From: MrAlders0n Date: Mon, 27 Jul 2026 19:10:07 -0400 Subject: [PATCH] Add the two-line packet table row --- src/features/packets/PacketTableRow.tsx | 72 +++++++++++++++++++ .../features/packets/PacketTableRow.test.tsx | 68 ++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 src/features/packets/PacketTableRow.tsx create mode 100644 tests/features/packets/PacketTableRow.test.tsx diff --git a/src/features/packets/PacketTableRow.tsx b/src/features/packets/PacketTableRow.tsx new file mode 100644 index 0000000..68b3212 --- /dev/null +++ b/src/features/packets/PacketTableRow.tsx @@ -0,0 +1,72 @@ +import { formatHex } from "../../lib/formatters"; +import { Timestamp } from "../../components/Timestamp"; +import { Badge } from "../../components/Badge"; +import { ScopeTag } from "../../components/ScopeTag"; +import { payloadTypeVariant } from "../../components/badge-utils"; +import { PAYLOAD_TYPE_NAMES, type PayloadTypeValue } from "../../types/enums"; +import type { PacketSummary } from "../../types/api"; +import { GRID_TEMPLATE } from "./packet-grid"; +import { PacketPathLine } from "./PacketPathLine"; + +interface PacketTableRowProps { + packet: PacketSummary; + expanded: boolean; + isFresh?: boolean; + onToggle: () => void; +} + +// two-line table row: line 1 is the identity grid (shares GRID_TEMPLATE with the header), line 2 +// is the latest path. The whole row is the expansion click target. +export function PacketTableRow({ packet, expanded, isFresh, onToggle }: PacketTableRowProps) { + const observer = packet.latestObserver; + + return ( +
+ + +
+ +
+
+ ); +} diff --git a/tests/features/packets/PacketTableRow.test.tsx b/tests/features/packets/PacketTableRow.test.tsx new file mode 100644 index 0000000..3e55fb2 --- /dev/null +++ b/tests/features/packets/PacketTableRow.test.tsx @@ -0,0 +1,68 @@ +import { describe, it, expect, vi } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { PacketTableRow } from "../../../src/features/packets/PacketTableRow"; +import type { PacketSummary } 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, +}); + +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("renders line 2 even when there is no path data, so row height is constant", () => { + render( {}} />); + expect(screen.getByText("n/a")).toBeInTheDocument(); + }); + + it("falls back to the observer id when there is no display name", () => { + render( {}} />); + expect(screen.getByText("abcdef12")).toBeInTheDocument(); + expect(screen.getByText("YVR")).toBeInTheDocument(); + }); + + it("prefers the observer display name over the id", () => { + render( {}} />); + expect(screen.getByText("Cypress Peak")).toBeInTheDocument(); + expect(screen.queryByText("abcdef12")).not.toBeInTheDocument(); + }); + + 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(); + }); +});