/** * Activity in history mode, through the real router: the URL is the applied * state, so nothing here can be checked by rendering the page on its own. */ import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import { QueryClientProvider } from "@tanstack/react-query"; import { RouterProvider, createMemoryHistory } from "@tanstack/react-router"; import { AuthProvider } from "@/auth/store"; import { createQueryClient } from "@/lib/queryClient"; import { createAppRouter } from "@/routes"; import { health } from "@/lib/healthFixture"; import type { Client, Coverage, QueriesPage, QueryRow } from "@/lib/types"; import { queryRow } from "@/features/provenance/provenanceFixture"; import { FakeEventSource } from "./fakeEventSource"; function client(id: number, ip: string, name: string, learnedName: string): Client { return { id, 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(1, "192.0.2.10", "Kitchen Pi", "pi.lan"), client(2, "192.0.2.11", "", "laptop.lan"), client(3, "192.0.2.12", "", ""), ]; function row(id: number, domain: string, overrides: Partial = {}): QueryRow { return queryRow(id, { ts: 1_700_000_000 + id, domain, upstream: "udp://9.9.9.9:53", ...overrides }); } const COMPLETE: Coverage = { complete: true, available_since: 1_600_000_000 }; /** The blocked row every page fixture reuses. */ const BLOCKED = { blocked: true, policy_action: "block", policy_reason: "blocklist_wildcard", route_kind: "blocked", upstream: "", } as const satisfies Partial; const PAGES: Record = { "/api/queries": { queries: [ row(20, "first.example", { qtype: 65, cache_hit: true, route_kind: "cache" }), row(19, "ads.example", { ...BLOCKED, response_time_us: null, cache_hit: null }), ], next_before: 19, coverage: COMPLETE, }, "/api/queries?before=19": { queries: [row(5, "older.example")], next_before: null, coverage: COMPLETE, }, "/api/queries?domain=ads": { queries: [row(19, "ads.example", BLOCKED)], next_before: null, coverage: COMPLETE, }, "/api/queries?domain=ads&blocked=true": { queries: [row(19, "ads.example", BLOCKED)], next_before: null, coverage: COMPLETE, }, "/api/queries?since=1700000000": { queries: [row(20, "first.example")], next_before: null, coverage: COMPLETE, }, }; let fetchMock: ReturnType; function json(payload: unknown): Response { return new Response(JSON.stringify(payload), { status: 200, headers: { "content-type": "application/json" } }); } const VERSION = { version: "0.0.0-test", zig_version: "0.16.0", uptime_seconds: 1 }; /** The shell's own requests, which every test serves the same way. */ function stubFetch(handler: (url: string) => Response | Promise) { 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 Promise.resolve(handler(url)); }); vi.stubGlobal("fetch", fetchMock); } function fromPages(url: string): Response { if (url === "/api/clients") return json({ clients: CLIENTS }); const payload = PAGES[url]; if (payload === undefined) return new Response(JSON.stringify({ error: "not stubbed" }), { status: 404 }); return json(payload); } beforeEach(() => { stubFetch(fromPages); }); afterEach(() => { vi.unstubAllGlobals(); }); function renderPage(path = "/activity") { const queryClient = createQueryClient(); const history = createMemoryHistory({ initialEntries: [path] }); const router = createAppRouter(history, queryClient); render( , ); return { queryClient, history }; } /** Every `/api/queries` URL the run asked for, list pages only. */ function queryCalls(): string[] { return fetchMock.mock.calls .map((call) => String(call[0])) .filter((url) => url === "/api/queries" || url.startsWith("/api/queries?")); } /** The toolbar's search field, which is how a domain filter is entered now. */ function domainInput(): HTMLInputElement { return screen.getByLabelText("Filter domains") as HTMLInputElement; } /** Enter in a text field: the debounce's escape hatch, and the fast path here. */ function submitFilters() { fireEvent.submit(domainInput().closest("form")!); } /** * The footer's count line, matched on the whole sentence. * * The number sits in a span of its own so it can carry tabular digits, so the * line is several text nodes and the default string matcher — which reads one * node at a time — cannot see it whole. */ function countMatcher(pattern: RegExp) { return (_: string, element: Element | null): boolean => element?.tagName === "P" && pattern.test(element.textContent ?? ""); } function countLine(pattern: RegExp): HTMLElement { return screen.getByText(countMatcher(pattern)); } function findCountLine(pattern: RegExp): Promise { return screen.findByText(countMatcher(pattern)); } /** The custom range lives behind the Time menu; the two bounds only exist there. */ function openCustomRange() { fireEvent.click(screen.getByRole("button", { name: /^Time: / })); fireEvent.click(screen.getByRole("menuitem", { name: "Custom…" })); } test("renders the first page with the seven columns filled in", async () => { renderPage(); await screen.findByText("first.example"); expect(screen.getAllByRole("columnheader").map((header) => header.textContent)).toEqual([ "Time", "Domain", "Client", "Type", "Result", "Route", "Duration", ]); const first = screen.getByText("first.example").closest("tr")!; expect(within(first).getByText("HTTPS")).toBeTruthy(); expect(within(first).getByText("NOERROR")).toBeTruthy(); expect(within(first).getByText("Cache")).toBeTruthy(); expect(within(first).getByText("1.2 ms")).toBeTruthy(); const blocked = screen.getByText("ads.example").closest("tr")!; // The Result cell says Blocked even though the client saw NOERROR, and the // Route cell says how: this is the pair the old Status column could not show. expect(within(blocked).getAllByText("Blocked")).toHaveLength(2); expect(within(blocked).getByText("—")).toBeTruthy(); expect(countLine(/Showing 2 queries/)).toBeTruthy(); }); test("resolves each row's client to its display name, reading the IP out with it", async () => { stubFetch((url) => { if (url === "/api/clients") return json({ clients: CLIENTS }); if (url !== "/api/queries") return new Response(JSON.stringify({ error: "not stubbed" }), { status: 404 }); return json({ queries: [ row(20, "named.example", { client_ip: "192.0.2.10" }), row(19, "learned.example", { client_ip: "192.0.2.11" }), row(18, "nameless.example", { client_ip: "192.0.2.12" }), row(17, "stranger.example", { client_ip: "192.0.2.99" }), ], next_before: null, coverage: COMPLETE, } satisfies QueriesPage); }); renderPage(); // A hand-typed name wins outright; the learned name never surfaces for it. const named = await screen.findByText("Kitchen Pi"); // The address reads out with the name it replaced, rather than sitting in a // title only a mouse can reach. expect(named.textContent).toBe("Kitchen Pi (192.0.2.10)"); expect(named.getAttribute("title")).toBeNull(); expect(screen.queryByText("pi.lan")).toBeNull(); // A learned name reads muted and nothing more here: the "learned" tag would // repeat on every row of the table, so the Clients page carries it instead. const learned = screen.getByText("laptop.lan"); expect(learned.textContent).toBe("laptop.lan (192.0.2.11)"); expect(within(learned.closest("tr")!).queryByText("learned")).toBeNull(); // A known client with neither name, and a client the loaded list has never // seen, both fall back to the bare address with nothing standing in for it. expect(screen.getByText("192.0.2.12").textContent).toBe("192.0.2.12"); expect(screen.getByText("192.0.2.99").textContent).toBe("192.0.2.99"); }); test("load more appends the next page and stops at the end of the log", async () => { renderPage(); await screen.findByText("first.example"); fireEvent.click(screen.getByRole("button", { name: "Load more" })); await screen.findByText("older.example"); expect(screen.getByText("first.example")).toBeTruthy(); expect(countLine(/Showing 3 queries — end of log/)).toBeTruthy(); expect(screen.queryByRole("button", { name: "Load more" })).toBeNull(); }); test("applying a filter puts it in the url, refetches, and resets the accumulated list", async () => { const { history } = renderPage(); await screen.findByText("first.example"); fireEvent.click(screen.getByRole("button", { name: "Load more" })); await screen.findByText("older.example"); fireEvent.change(domainInput(), { target: { value: "ads" } }); submitFilters(); await findCountLine(/Showing 1 query /); expect(history.location.search).toContain("domain=ads"); expect(screen.getByText("ads.example")).toBeTruthy(); expect(screen.queryByText("first.example")).toBeNull(); expect(screen.queryByText("older.example")).toBeNull(); }); test("a load-more that resolves after a filter change is discarded", async () => { let releaseLoadMore: () => void = () => {}; stubFetch((url) => { if (url === "/api/queries?before=19") { return new Promise((resolve) => { releaseLoadMore = () => resolve(json(PAGES["/api/queries?before=19"])); }); } return fromPages(url); }); renderPage(); await screen.findByText("first.example"); fireEvent.click(screen.getByRole("button", { name: "Load more" })); fireEvent.change(domainInput(), { target: { value: "ads" } }); submitFilters(); await findCountLine(/Showing 1 query /); releaseLoadMore(); await act(async () => { await new Promise((resolve) => setTimeout(resolve, 0)); }); expect(screen.queryByText("older.example")).toBeNull(); expect(countLine(/Showing 1 query /)).toBeTruthy(); expect(screen.queryByRole("alert")).toBeNull(); }); test("load more is disabled while a filter change shows placeholder data, then uses the fresh cursor", async () => { let releaseFiltered: () => void = () => {}; const filteredPage: QueriesPage = { queries: [row(19, "ads.example", BLOCKED)], next_before: 7, coverage: COMPLETE, }; const filteredOlderPage: QueriesPage = { queries: [row(3, "ads.older.example")], next_before: null, coverage: COMPLETE, }; stubFetch((url) => { if (url === "/api/queries?domain=ads") { return new Promise((resolve) => { releaseFiltered = () => resolve(json(filteredPage)); }); } if (url === "/api/queries?domain=ads&before=7") return json(filteredOlderPage); return fromPages(url); }); renderPage(); await screen.findByText("first.example"); fireEvent.change(domainInput(), { target: { value: "ads" } }); submitFilters(); const staleButton = await screen.findByRole("button", { name: "Load more" }); expect(staleButton).toHaveProperty("disabled", true); fireEvent.click(staleButton); expect(queryCalls()).not.toContain("/api/queries?domain=ads&before=19"); releaseFiltered(); await waitFor(() => { expect(screen.queryByText("first.example")).toBeNull(); }); const freshButton = screen.getByRole("button", { name: "Load more" }); expect(freshButton).toHaveProperty("disabled", false); fireEvent.click(freshButton); await screen.findByText("ads.older.example"); expect(queryCalls()).toContain("/api/queries?domain=ads&before=7"); expect(countLine(/Showing 2 queries — end of log/)).toBeTruthy(); }); test("a background refetch after new rows arrive leaves no gap between the loaded pages", async () => { // The newest-100 window moves up while the reader has a second page open. // Refetching only the first page would drop n20 and n19 out of the middle // of the table; the second page must be replayed from the fresh cursor. const before: Record = { "/api/queries": { queries: [row(20, "n20.example"), row(19, "n19.example")], next_before: 19, coverage: COMPLETE, }, "/api/queries?before=19": { queries: [row(18, "n18.example"), row(17, "n17.example")], next_before: null, coverage: COMPLETE, }, }; const after: Record = { "/api/queries": { queries: [row(22, "n22.example"), row(21, "n21.example")], next_before: 21, coverage: COMPLETE, }, "/api/queries?before=21": { queries: [row(20, "n20.example"), row(19, "n19.example"), row(18, "n18.example"), row(17, "n17.example")], next_before: null, coverage: COMPLETE, }, }; let live = before; stubFetch((url) => { const payload = live[url]; if (payload === undefined) return new Response(JSON.stringify({ error: "not stubbed" }), { status: 404 }); return json(payload); }); const { queryClient } = renderPage(); await screen.findByText("n20.example"); fireEvent.click(screen.getByRole("button", { name: "Load more" })); await screen.findByText("n17.example"); live = after; await act(async () => { await queryClient.invalidateQueries({ queryKey: ["queries"] }); }); await screen.findByText("n22.example"); const shown = screen.getAllByText(/^n\d+\.example$/).map((cell) => cell.textContent); expect(shown).toEqual(["n22.example", "n21.example", "n20.example", "n19.example", "n18.example", "n17.example"]); expect(countLine(/Showing 6 queries — end of log/)).toBeTruthy(); }); test("a 401 on load more routes through handleUnauthorized instead of the inline error", async () => { const assign = vi.fn(); vi.stubGlobal("location", { pathname: "/activity", search: "", assign }); stubFetch((url) => { if (url === "/api/queries?before=19") { return new Response(JSON.stringify({ error: "unauthorized" }), { status: 401, headers: { "content-type": "application/json" }, }); } return fromPages(url); }); renderPage(); await screen.findByText("first.example"); fireEvent.click(screen.getByRole("button", { name: "Load more" })); await waitFor(() => { expect(assign).toHaveBeenCalledWith(`/login?redirect=${encodeURIComponent("/activity")}`); }); expect(screen.queryByRole("alert")).toBeNull(); expect(screen.queryByText(/Failed to load more/)).toBeNull(); }); test("each row links into its own detail page, carrying the investigation with it", async () => { renderPage("/activity?mode=history&since=1700000000"); await screen.findByText("first.example"); const link = screen.getByRole("link", { name: "first.example" }); expect(link.getAttribute("href")).toContain("/activity/queries/20"); expect(link.getAttribute("href")).toContain("since=1700000000"); // An is in the tab order by default; nothing here may opt it out. expect(link.getAttribute("tabindex")).toBeNull(); }); test("a pruned window tells the reader when history starts", async () => { stubFetch((url) => { if (url === "/api/clients") return json({ clients: CLIENTS }); if (url !== "/api/queries") return new Response(JSON.stringify({ error: "not stubbed" }), { status: 404 }); return json({ queries: [row(20, "kept.example")], next_before: null, coverage: { complete: false, available_since: 1_700_000_000 }, } satisfies QueriesPage); }); renderPage(); await screen.findByText("kept.example"); expect(screen.getByText(/Query history is available from/)).toBeTruthy(); }); test("a complete window shows no coverage notice", async () => { renderPage(); await screen.findByText("first.example"); expect(screen.queryByText(/Query history is available from/)).toBeNull(); }); test("a ?domain= link seeds the filter form and fetches that domain on arrival", async () => { renderPage("/activity?domain=ads"); await screen.findByText("ads.example"); expect(domainInput()).toHaveProperty("value", "ads"); expect(screen.queryByText("first.example")).toBeNull(); }); test("history forwards exactly the six normalized filter fields and nothing else", async () => { stubFetch((url) => { if (url === "/api/clients") return json({ clients: CLIENTS }); return json({ queries: [], next_before: null, coverage: COMPLETE } satisfies QueriesPage); }); renderPage( "/activity?mode=history&domain=%20ads%20&client=192.0.2.5&blocked=true&since=1700000000&until=1700000600&bogus=1&limit=9999", ); await screen.findByText("No queries match the current filters."); expect(queryCalls()).toEqual([ "/api/queries?domain=ads&client=192.0.2.5&blocked=true&since=1700000000&until=1700000600", ]); }); test("a rejected search parameter is dropped rather than guessed at", async () => { renderPage("/activity?since=1.5&blocked=%22true%22&domain=%20%20"); await screen.findByText("first.example"); // Nothing survived validation, so the request is the unfiltered one. expect(queryCalls()).toEqual(["/api/queries"]); expect(domainInput()).toHaveProperty("value", ""); }); test("the form draft follows the url back and forward, seconds included", async () => { stubFetch((url) => { if (url === "/api/clients") return json({ clients: CLIENTS }); return json({ queries: [], next_before: null, coverage: COMPLETE } satisfies QueriesPage); }); // A bound with non-zero seconds: the round trip has to keep them, and the // untouched field has to carry the original number rather than re-parse. const seeded = 1_700_000_017; const { history } = renderPage(`/activity?mode=history&domain=first&since=${seeded}`); await screen.findByLabelText("Filter domains"); expect(domainInput()).toHaveProperty("value", "first"); // A seeded custom range opens its row, so the link's bounds are visible. const sinceInput = screen.getByLabelText("Since") as HTMLInputElement; expect(sinceInput.value).toContain(":37"); fireEvent.change(domainInput(), { target: { value: "second" } }); submitFilters(); await waitFor(() => expect(history.location.search).toContain("domain=second")); // The untouched Since bound applied as the exact second it was seeded with. expect(queryCalls()).toContain(`/api/queries?domain=second&since=${seeded}`); // A pasted link, then the buttons over it: the draft is derived from the URL, // so whichever way the browser moves it the field has to move with it. act(() => history.push(`/activity?mode=history&domain=third&since=${seeded}`)); await waitFor(() => { expect(domainInput()).toHaveProperty("value", "third"); }); act(() => history.back()); await waitFor(() => { expect(domainInput()).toHaveProperty("value", "second"); }); act(() => history.forward()); await waitFor(() => { expect(domainInput()).toHaveProperty("value", "third"); }); }); /** 2 a.m. on the EU spring-forward date: an hour that exists in some zones and not others. */ const DST_WALL_TIME = "2026-03-29T02:30:00"; /** * Whether that wall time names an instant in the timezone the suite runs in. * `new Date` slides a spring-forward gap silently forward, so an hour or minute * that comes back different from the one written *is* the gap. */ function wallTimeExists(text: string): boolean { const written = /T(\d{2}):(\d{2})/.exec(text)!; const parsed = new Date(text); return parsed.getHours() === Number(written[1]) && parsed.getMinutes() === Number(written[2]); } test("a wall-clock time the daylight-saving jump skips is refused, not silently moved", async () => { // One expected outcome per timezone, decided here rather than accepted from // the page: in a zone with the jump the bound must be refused outright, and // in a zone without it the same text is an ordinary instant that applies. const inGap = !wallTimeExists(DST_WALL_TIME); const unix = Math.floor(new Date(DST_WALL_TIME).getTime() / 1000); stubFetch((url) => { if (url === `/api/queries?since=${unix}`) { return json({ queries: [], next_before: null, coverage: COMPLETE } satisfies QueriesPage); } return fromPages(url); }); const { history } = renderPage(); await screen.findByText("first.example"); const callsBefore = queryCalls().length; const searchBefore = history.location.search; openCustomRange(); fireEvent.change(screen.getByLabelText("Since"), { target: { value: DST_WALL_TIME } }); fireEvent.click(screen.getByRole("button", { name: "Set range" })); if (inGap) { expect(screen.getByRole("alert").textContent).toContain("daylight saving"); // Refused means refused: no navigation, and no request for the hour the // operator did not ask for. expect(history.location.search).toBe(searchBefore); expect(queryCalls()).toHaveLength(callsBefore); return; } await waitFor(() => expect(history.location.search).toContain(`since=${unix}`)); expect(queryCalls()).toContain(`/api/queries?since=${unix}`); expect(screen.queryByRole("alert")).toBeNull(); }); test("the policy simulation is reachable from the header, with no rows to click through", async () => { stubFetch((url) => { if (url === "/api/clients") return json({ clients: CLIENTS }); if (url === "/api/groups") return json({ groups: [{ id: 1, name: "default", safe_search: false }] }); return json({ queries: [], next_before: null, coverage: COMPLETE } satisfies QueriesPage); }); const { history } = renderPage(); // An empty log is exactly the case a row-borne link cannot serve. await screen.findByText("No queries logged yet."); const link = screen.getByRole("link", { name: "Current policy simulation" }); expect(link.getAttribute("href")).toBe("/activity/test"); fireEvent.click(link); await screen.findByRole("heading", { level: 1, name: "Current policy simulation" }); expect(history.location.pathname).toBe("/activity/test"); }); test("the mode switch is a tab list whose selection is the url, and it keeps the filters", async () => { // Live opens a stream as soon as its panel mounts, so the switch cannot be // exercised without one; the fake stands in for the browser's EventSource. vi.stubGlobal( "EventSource", class { constructor(url: string) { return new FakeEventSource(url) as unknown as EventSource; } }, ); const { history } = renderPage("/activity?mode=history&domain=ads&blocked=true"); await screen.findByText("ads.example"); const tabs = within(screen.getByRole("tablist", { name: "Activity mode" })); expect(tabs.getAllByRole("tab").map((tab) => tab.textContent)).toEqual(["History", "Live"]); expect(tabs.getByRole("tab", { name: "History", selected: true })).toBeTruthy(); expect(tabs.getByRole("tab", { name: "Live", selected: false })).toBeTruthy(); fireEvent.click(tabs.getByRole("tab", { name: "Live" })); await waitFor(() => expect(history.location.search).toContain("mode=live")); // The investigation survives the switch: both filters are still in the URL. expect(history.location.search).toContain("domain=ads"); expect(history.location.search).toContain("blocked=true"); expect( within(screen.getByRole("tablist", { name: "Activity mode" })).getByRole("tab", { name: "Live", selected: true, }), ).toBeTruthy(); expect(await screen.findByText(/the History filters apply to history only/)).toBeTruthy(); }); test("Clear empties the url as well as the form", async () => { const { history } = renderPage("/activity?mode=history&domain=ads&blocked=true"); await screen.findByText("ads.example"); fireEvent.click(screen.getByRole("button", { name: "Clear" })); await waitFor(() => { expect(history.location.search).not.toContain("domain"); }); expect(history.location.search).not.toContain("blocked"); expect(domainInput()).toHaveProperty("value", ""); }); test("the empty result offers the clear it names, and it clears the same filters the toolbar does", async () => { stubFetch((url) => { if (url === "/api/clients") return json({ clients: CLIENTS }); if (url === "/api/queries") return json(PAGES["/api/queries"]); return json({ queries: [], next_before: null, coverage: COMPLETE } satisfies QueriesPage); }); const { history } = renderPage("/activity?mode=history&domain=nothing&blocked=true"); await screen.findByText("No queries match the current filters."); fireEvent.click(screen.getByRole("button", { name: "Clear filters" })); await waitFor(() => { expect(history.location.search).not.toContain("domain"); }); expect(history.location.search).not.toContain("blocked"); expect(domainInput()).toHaveProperty("value", ""); // The unfiltered log is back, so the button did the toolbar's Clear and not a // reset of its own that only emptied the form. expect(await screen.findByText("first.example")).toBeTruthy(); }); test("an empty log offers no clear, because there is no filter to blame for it", async () => { stubFetch((url) => { if (url === "/api/clients") return json({ clients: CLIENTS }); return json({ queries: [], next_before: null, coverage: COMPLETE } satisfies QueriesPage); }); renderPage(); await screen.findByText("No queries logged yet."); expect(screen.queryByRole("button", { name: "Clear filters" })).toBeNull(); }); test("the domain field debounces into the url, and Enter flushes it at once", async () => { vi.useFakeTimers({ shouldAdvanceTime: true }); try { const { history } = renderPage(); await screen.findByText("first.example"); fireEvent.change(domainInput(), { target: { value: "a" } }); fireEvent.change(domainInput(), { target: { value: "ad" } }); fireEvent.change(domainInput(), { target: { value: "ads" } }); // Mid-word the URL has not moved: three keystrokes are one investigation, // not three, and each one would otherwise be a request and a history entry. expect(history.location.search).not.toContain("domain"); await act(async () => { await vi.advanceTimersByTimeAsync(400); }); expect(history.location.search).toContain("domain=ads"); // Replaced, not pushed: Back leaves the page, it does not retype the word. expect(history.length).toBe(1); fireEvent.change(domainInput(), { target: { value: "first" } }); submitFilters(); await waitFor(() => expect(history.location.search).toContain("domain=first")); } finally { vi.useRealTimers(); } }); test("the field being typed in keeps the focus when the debounce commits", async () => { vi.useFakeTimers({ shouldAdvanceTime: true }); try { const { history } = renderPage(); await screen.findByText("first.example"); const input = domainInput(); input.focus(); fireEvent.change(input, { target: { value: "ads" } }); await act(async () => { await vi.advanceTimersByTimeAsync(400); }); await waitFor(() => expect(history.location.search).toContain("domain=ads")); // The same node, still focused, still holding the caret: a toolbar that // remounted on the URL it just wrote would drop the next keystroke. expect(domainInput()).toBe(input); expect(document.activeElement).toBe(input); } finally { vi.useRealTimers(); } }); test("a result segment commits on the click, with no wait and no button", async () => { const { history } = renderPage("/activity?domain=ads"); await screen.findByText("ads.example"); fireEvent.click(screen.getByRole("radio", { name: "Blocked" })); await waitFor(() => expect(history.location.search).toContain("blocked=true")); expect(history.location.search).toContain("domain=ads"); }); test("a time preset writes the second it resolved to, not a rolling window", async () => { const now = 1_700_000_000_000; vi.spyOn(Date, "now").mockReturnValue(now); stubFetch((url) => { if (url === "/api/clients") return json({ clients: CLIENTS }); return json({ queries: [], next_before: null, coverage: COMPLETE } satisfies QueriesPage); }); const { history } = renderPage(); await screen.findByText("No queries logged yet."); fireEvent.click(screen.getByRole("button", { name: "Time: Any time" })); fireEvent.click(screen.getByRole("menuitem", { name: "Past hour" })); // Both bounds are concrete, so the link names a closed hour rather than one // that keeps growing at the top as the log does. const since = now / 1000 - 3600; const until = now / 1000; await waitFor(() => expect(history.location.search).toContain(`since=${since}`)); expect(history.location.search).toContain(`until=${until}`); expect(screen.getByRole("button", { name: "Time: Past hour" })).toBeTruthy(); expect(queryCalls()).toContain(`/api/queries?since=${since}&until=${until}`); vi.restoreAllMocks(); }); test("Live shows no toolbar at all", async () => { vi.stubGlobal( "EventSource", class { constructor(url: string) { return new FakeEventSource(url) as unknown as EventSource; } }, ); renderPage("/activity?mode=live&domain=ads"); await screen.findByText(/the History filters apply to history only/); // The stream is unfiltered, so a control here would promise filtering that is // not happening; the filters are still in the URL, waiting for History. expect(screen.queryByLabelText("Filter domains")).toBeNull(); expect(screen.queryByRole("radio", { name: "Blocked" })).toBeNull(); expect(screen.queryByRole("button", { name: /^Time: / })).toBeNull(); }); test("the coverage watermark reads under the results, never over them", async () => { stubFetch((url) => { if (url === "/api/clients") return json({ clients: CLIENTS }); if (url !== "/api/queries") return new Response(JSON.stringify({ error: "not stubbed" }), { status: 404 }); return json({ queries: [row(20, "kept.example")], next_before: null, coverage: { complete: false, available_since: 1_700_000_000 }, } satisfies QueriesPage); }); renderPage(); await screen.findByText("kept.example"); const watermark = screen.getByText(/Query history is available from/); const count = countLine(/Showing 1 query/); // After the count in document order, which is what "footer" means here. expect(count.compareDocumentPosition(watermark) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); });