import { expect, it, vi } from "vitest"; import { fireEvent, render, screen } from "@testing-library/react"; import "../../../src/i18n"; import { ObserverSidebar } from "../../../src/features/observers/ObserverSidebar"; import { directoryRow } from "../../fixtures/observer-directory"; const props = { observers: [directoryRow("Charlie", 900), directoryRow("Alpha", 100), directoryRow("Zero", 0), directoryRow("Unavailable", null)], filtered: false, isPending: false, isError: false, unsupported: false, onRetry: vi.fn(), sort: "traffic" as const, onSortChange: vi.fn(), maxObservationCount: 1000, hasNextPage: true, isFetchingNextPage: false, onLoadMore: vi.fn(), selectedId: "Alpha", onSelect: vi.fn(), }; it("preserves server order and distinguishes known zero from unavailable counts", () => { render(); const rows = screen.getAllByRole("option"); expect(rows.map(row => row.textContent)).toEqual(["Receiver Charlie900", "Receiver Alpha100", "Receiver Zero0", "Receiver Unavailable-"]); expect(rows[1]).toHaveAttribute("aria-selected", "true"); expect(rows[1].querySelector('[style]')).toHaveStyle({ width: "10%" }); expect(rows[2].querySelector('[style]')).toHaveStyle({ width: "0%" }); expect(rows[3].querySelector('[style]')).toBeNull(); fireEvent.click(rows[0]); expect(props.onSelect).toHaveBeenCalledWith("Charlie"); }); it("requests server Name sorting and keeps normalization fixed when pages append", () => { const { rerender } = render(); fireEvent.click(screen.getByRole("button", { name: "Name" })); expect(props.onSortChange).toHaveBeenCalledWith("name"); rerender(); expect(screen.getByRole("option", { name: /Receiver Alpha/ }).querySelector('[style]')).toHaveStyle({ width: "10%" }); expect(screen.getByRole("option", { name: /Receiver Later/ }).querySelector('[style]')).toHaveStyle({ width: "100%" }); }); it("loads near the scroll end but not at the top of a long list or while loading", () => { const onLoadMore = vi.fn(); const { rerender } = render(); const scroll = screen.getByRole("listbox").parentElement!; Object.defineProperties(scroll, { clientHeight: { value: 400 }, scrollHeight: { value: 2000 }, scrollTop: { value: 0, writable: true } }); fireEvent.scroll(scroll); expect(onLoadMore).not.toHaveBeenCalled(); scroll.scrollTop = 1500; fireEvent.scroll(scroll); expect(onLoadMore).toHaveBeenCalledTimes(1); rerender(); fireEvent.scroll(scroll); expect(onLoadMore).toHaveBeenCalledTimes(1); }); it("fills a short viewport automatically, and stops on errors or the final page", () => { const height = vi.spyOn(HTMLElement.prototype, "clientHeight", "get").mockReturnValue(500); const scrollHeight = vi.spyOn(HTMLElement.prototype, "scrollHeight", "get").mockReturnValue(100); const onLoadMore = vi.fn(); try { const { rerender } = render(); expect(onLoadMore).toHaveBeenCalledTimes(1); rerender(); expect(onLoadMore).toHaveBeenCalledTimes(1); expect(screen.getAllByRole("option")).toHaveLength(4); fireEvent.click(screen.getByRole("button", { name: /Retry/ })); expect(props.onRetry).toHaveBeenCalled(); rerender(); expect(onLoadMore).toHaveBeenCalledTimes(1); expect(screen.getByText("All observers loaded")).toBeInTheDocument(); } finally { height.mockRestore(); scrollHeight.mockRestore(); } }); it("explains incomplete analytics and the server's effective Name order", () => { render(); expect(screen.getByText(/Traffic counts unavailable/)).toHaveTextContent(/Name order/); expect(screen.getAllByRole("option")[0]).toHaveTextContent("Receiver Charlie-"); expect(screen.getAllByRole("option")[0].querySelector('[style]')).toBeNull(); }); it("shows upgrade and retry, loading, and empty-filter states", () => { const { rerender } = render(); expect(screen.getByText(/server upgrade/)).toBeInTheDocument(); expect(screen.getByRole("button", { name: /Retry/ })).toBeInTheDocument(); rerender(); expect(screen.getByText("Loading…")).toBeInTheDocument(); rerender(); expect(screen.getByText("No matches")).toBeInTheDocument(); }); it.each(["partial", "unavailable"] as const)("renders available traffic and zero counts without a Name fallback when coverage is %s", status => { render(); const rows = screen.getAllByRole("option"); expect(rows.map(row => row.textContent)).toEqual(["Receiver Charlie900", "Receiver Alpha100", "Receiver Zero0"]); expect(rows[0].querySelector('[style]')).toHaveStyle({ width: "90%" }); expect(rows[2].querySelector('[style]')).toHaveStyle({ width: "0%" }); expect(screen.queryByText(/counts unavailable/i)).not.toBeInTheDocument(); expect(screen.queryByText(/Name order/)).not.toBeInTheDocument(); expect(screen.getByText(/More information: Counts use available history; coverage is incomplete\. Available history, up to 7 days Counts through 2026-10-0\d \d\d:00 UTC/)).toBeInTheDocument(); expect(screen.queryByText("Available history, up to 7 days")).not.toBeInTheDocument(); }); it("keeps the history notes in the info tip and the panel title off the phone layout", () => { render(); expect(screen.getByText(/^More information: Available history, up to 7 days Counts through/)).toBeInTheDocument(); expect(screen.queryByText(/coverage is incomplete/)).not.toBeInTheDocument(); expect(screen.getByText("Observers")).toHaveClass("hidden", "md:block"); const controls = screen.getByRole("button", { name: "Refresh" }).parentElement!; expect(controls).toContainElement(screen.getByRole("button", { name: "Name" })); expect(controls).toHaveClass("ml-auto"); expect(controls.parentElement).toHaveClass("flex-wrap"); });