/** * Activity in live mode, through the real router. * * The EventSource is a global here rather than an injected factory: whether the * connection exists at all is the thing under test, and that is decided by * which subtree the URL mounts, not by a prop a caller could pass. */ import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import { QueryClientProvider } from "@tanstack/react-query"; import { RouterContextProvider, RouterProvider, createMemoryHistory } from "@tanstack/react-router"; import { AuthProvider } from "@/auth/store"; import { createQueryClient } from "@/lib/queryClient"; import { createAppRouter } from "@/routes"; import type { Client } from "@/lib/types"; import { provenance, queryRow } from "@/features/provenance/provenanceFixture"; import { health } from "@/lib/healthFixture"; import { FakeEventSource } from "./fakeEventSource"; import LiveActivity from "./LiveActivity"; import type { ActivitySearch } from "./search"; const LIVE_ORIGIN: ActivitySearch = { mode: "live", since: undefined, until: undefined, domain: undefined, client: undefined, blocked: undefined, }; function client(ip: string, name: string, learnedName: string): Client { return { id: Number(ip.split(".").pop()), ip, name, learned_name: learnedName, group_id: 1, group: "default", hand_edited: name !== "", first_seen: 1_700_000_000, last_seen: 1_700_000_100, }; } const CLIENTS: Client[] = [ client("192.0.2.10", "Kitchen Pi", "pi.lan"), client("192.0.2.11", "", "laptop.lan"), client("192.0.2.12", "", ""), ]; const VERSION = { version: "0.0.0-test", git_commit: "0000000", zig_version: "0.16.0", uptime_seconds: 1 }; let sources: FakeEventSource[]; let fetchMock: ReturnType; function json(payload: unknown): Response { return new Response(JSON.stringify(payload), { status: 200, headers: { "content-type": "application/json" } }); } function stubFetch(handler: (url: string) => Response | Promise = () => json({})) { fetchMock = vi.fn((input: RequestInfo | URL) => { const url = String(input); if (url === "/api/version") return Promise.resolve(json(VERSION)); if (url === "/api/clients") return Promise.resolve(json({ clients: CLIENTS })); // The shell reads health on every route for the Diagnostics nav badge. if (url === "/api/health") return Promise.resolve(json(health())); return Promise.resolve(handler(url)); }); vi.stubGlobal("fetch", fetchMock); } beforeEach(() => { sources = []; vi.stubGlobal( "EventSource", class { constructor(url: string) { const source = new FakeEventSource(url); sources.push(source); return source as unknown as EventSource; } }, ); stubFetch(); }); afterEach(() => { vi.unstubAllGlobals(); }); function frame(ts: number, domain: string, sections: Parameters[0] = {}): { data: string } { return { data: JSON.stringify( provenance({ ...sections, request: { time: ts, domain, ...sections.request }, route: { kind: "cache", upstream: "", ...sections.route }, }), ), }; } function renderPage(path = "/activity?mode=live") { const queryClient = createQueryClient(); const history = createMemoryHistory({ initialEntries: [path] }); const router = createAppRouter(history, queryClient); render( , ); return { history }; } async function openLive(path?: string) { const rendered = renderPage(path); await screen.findByRole("button", { name: "Freeze" }); act(() => sources[0]!.emit("open")); return rendered; } function queryCalls(): string[] { return fetchMock.mock.calls .map((call) => String(call[0])) .filter((url) => url === "/api/queries" || url.startsWith("/api/queries?")); } test("streams rows, flags blocked ones, and freezes the display", async () => { await openLive(); expect(screen.getByRole("status", { name: "Live" })).toBeTruthy(); expect(screen.getByText("Waiting for queries…")).toBeTruthy(); act(() => { sources[0]!.emit("query", frame(1000, "ok.example")); sources[0]!.emit( "query", frame(1001, "ads.example", { request: { qtype: 28 }, policy: { action: "block", reason: "blocklist_wildcard" }, route: { kind: "blocked" }, }), ); }); expect(screen.getByText("ok.example")).toBeTruthy(); expect(screen.getByText("AAAA")).toBeTruthy(); const blockedRow = screen.getByText("ads.example").closest("tr")!; expect(within(blockedRow).getAllByText("Blocked")).toHaveLength(2); // StyleX compiles to opaque class names, so the check is structural: a blocked // row carries every class a plain row does, plus the ones the flag adds. const plainRow = screen.getByText("ok.example").closest("tr")!; const blockedClasses = new Set(blockedRow.className.split(" ")); const plainClasses = plainRow.className.split(" "); expect(plainClasses.every((name) => blockedClasses.has(name))).toBe(true); expect(blockedClasses.size).toBeGreaterThan(plainClasses.length); const freeze = screen.getByRole("button", { name: "Freeze" }); fireEvent.click(freeze); expect(freeze.getAttribute("aria-pressed")).toBe("true"); act(() => sources[0]!.emit("query", frame(1002, "later.example"))); expect(screen.queryByText("later.example")).toBeNull(); expect(screen.getByText(/3 in buffer/)).toBeTruthy(); fireEvent.click(screen.getByRole("button", { name: "Resume" })); expect(screen.getByText("later.example")).toBeTruthy(); }); test("resolves each row's client to its display name, keeping the IP as the tooltip", async () => { await openLive(); act(() => { sources[0]!.emit("query", frame(1000, "named.example", { request: { client: "192.0.2.10" } })); sources[0]!.emit("query", frame(1001, "learned.example", { request: { client: "192.0.2.11" } })); sources[0]!.emit("query", frame(1002, "nameless.example", { request: { client: "192.0.2.12" } })); sources[0]!.emit("query", frame(1003, "stranger.example", { request: { client: "192.0.2.99" } })); }); const named = await screen.findByText("Kitchen Pi"); expect(named.getAttribute("title")).toBe("192.0.2.10"); expect(screen.queryByText("pi.lan")).toBeNull(); const learned = screen.getByText("laptop.lan"); expect(learned.getAttribute("title")).toBe("192.0.2.11"); expect(within(learned.closest("tr")!).queryByText("learned")).toBeNull(); expect(screen.getByText("192.0.2.12").getAttribute("title")).toBeNull(); expect(screen.getByText("192.0.2.99").getAttribute("title")).toBeNull(); }); test("rows stream in as bare IPs while the client list is still loading", async () => { let releaseClients: () => void = () => {}; fetchMock = vi.fn((input: RequestInfo | URL) => { const url = String(input); if (url === "/api/version") return Promise.resolve(json(VERSION)); // The shell reads health on every route for the Diagnostics nav badge. if (url === "/api/health") return Promise.resolve(json(health())); return new Promise((resolve) => { if (url !== "/api/clients") { resolve(json({})); return; } releaseClients = () => resolve(json({ clients: CLIENTS })); }); }); vi.stubGlobal("fetch", fetchMock); await openLive(); act(() => sources[0]!.emit("query", frame(1000, "named.example", { request: { client: "192.0.2.10" } }))); expect(screen.getByText("192.0.2.10")).toBeTruthy(); expect(screen.queryByText("Kitchen Pi")).toBeNull(); releaseClients(); expect(await screen.findByText("Kitchen Pi")).toBeTruthy(); }); test("repeated connection failures show the viewer-cap state with a retry button", async () => { renderPage(); await screen.findByRole("button", { name: "Freeze" }); act(() => { sources[0]!.emit("error"); sources[0]!.emit("error"); sources[0]!.emit("error"); }); expect(screen.getByRole("alert").textContent).toContain("too many live viewers"); fireEvent.click(screen.getByRole("button", { name: "Retry" })); expect(sources).toHaveLength(2); expect(screen.getByText("Connecting…")).toBeTruthy(); }); test("a recovered row links to its stored detail; a streamed one opens in place instead", async () => { stubFetch((url) => { if (url.startsWith("/api/queries?")) { return json({ queries: [queryRow(88, { ts: 1001, domain: "recovered.example" })], next_before: null, coverage: { complete: true, available_since: 0 }, }); } return json({}); }); await openLive(); act(() => sources[0]!.emit("query", frame(1000, "streamed.example"))); act(() => sources[0]!.emit("error")); act(() => sources[0]!.emit("open")); const recovered = await screen.findByRole("link", { name: "recovered.example" }); expect(recovered.getAttribute("href")).toContain("/activity/queries/88"); // The streamed frame precedes its own insert, so it has no row to link to — // but it does carry its own provenance, so it still has a detail. expect(screen.queryByRole("link", { name: "streamed.example" })).toBeNull(); expect(screen.getByRole("button", { name: "streamed.example" })).toBeTruthy(); }); test("a streamed row opens its own provenance, from the keyboard as well as the pointer", async () => { await openLive(); act(() => sources[0]!.emit( "query", frame(1000, "streamed.example", { policy: { action: "block", reason: "blocklist_domain", matched: "streamed.example" }, route: { kind: "blocked", upstream: "" }, }), ), ); const trigger = screen.getByRole("button", { name: "streamed.example" }); // A real