import { fireEvent, render, screen } 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 type { LookupResult } from "@/lib/types"; const BLOCKED: LookupResult = { domain: "ads.example", group_id: 1, local_records: false, forward_zone: null, blocked: true, reason: "blocklist_domain", matched: "ads.example", source_url: "https://lists.test/a", safe_search_rewrite: null, }; let fetchMock: ReturnType; /** What `/api/lookup` answers, so a test can make it fail without rebuilding the mock. */ let lookup: (url: string) => Response; function json(payload: unknown, status = 200, headers: Record = {}): Response { return new Response(JSON.stringify(payload), { status, headers: { "content-type": "application/json", ...headers }, }); } function createFetchMock() { return vi.fn(async (input: RequestInfo | URL) => { const url = String(input); if (url === "/api/groups") { return json({ groups: [ { id: 1, name: "default", safe_search: false }, { id: 2, name: "kids", safe_search: true }, ], }); } if (url.startsWith("/api/lookup")) return lookup(url); return json({ error: "not stubbed" }, 404); }); } beforeEach(() => { lookup = (url) => url === "/api/lookup?domain=ads.example&group_id=1" ? json(BLOCKED) : json({ error: "not stubbed" }, 404); fetchMock = createFetchMock(); vi.stubGlobal("fetch", fetchMock); }); afterEach(() => { vi.unstubAllGlobals(); }); /** * `retry: false` for the failure tests: the shared client retries a 5xx twice * and a 429 after its Retry-After, so the surfaced error is what the page does * once the client has given up, not something a test should sit out in real * time. */ function renderPage(path = "/activity/test", { retry = true } = {}) { const queryClient = createQueryClient(); if (!retry) { const defaults = queryClient.getDefaultOptions(); queryClient.setDefaultOptions({ ...defaults, queries: { ...defaults.queries, retry: false } }); } const router = createAppRouter(createMemoryHistory({ initialEntries: [path] }), queryClient); render( , ); } function lookupCalls(): string[] { return fetchMock.mock.calls.map(([input]) => String(input)).filter((url) => url.startsWith("/api/lookup")); } test("fetches nothing until submit, then renders the blocked verdict", async () => { renderPage(); await screen.findByRole("heading", { name: "Current policy simulation" }); await screen.findByLabelText("Group"); expect(lookupCalls()).toEqual([]); fireEvent.change(screen.getByLabelText("Domain"), { target: { value: "ads.example" } }); expect(lookupCalls()).toEqual([]); fireEvent.click(screen.getByRole("button", { name: "Simulate" })); await screen.findByRole("heading", { name: "Blocked" }); expect(lookupCalls()).toEqual(["/api/lookup?domain=ads.example&group_id=1"]); expect(screen.getByText("blocklist_domain")).toBeTruthy(); const link = screen.getByRole("link", { name: "https://lists.test/a" }) as HTMLAnchorElement; expect(link.href).toBe("https://lists.test/a"); expect(screen.getByText("Queries for this name get a blocked response.")).toBeTruthy(); }); test("a ?domain= link asks the question on arrival instead of leaving a filled-in form", async () => { renderPage("/activity/test?domain=ads.example"); // No submit here: the link is the question, so the verdict is what arrives. await screen.findByRole("heading", { name: "Blocked" }); expect(lookupCalls()).toEqual(["/api/lookup?domain=ads.example&group_id=1"]); expect(screen.getByLabelText("Domain")).toHaveProperty("value", "ads.example"); }); test("no filter snapshot reads as a server that is starting, not as a verdict", async () => { lookup = () => json({ error: "no snapshot" }, 503); renderPage("/activity/test", { retry: false }); fireEvent.change(await screen.findByLabelText("Domain"), { target: { value: "ads.example" } }); fireEvent.click(screen.getByRole("button", { name: "Simulate" })); const alert = await screen.findByRole("alert"); expect(alert.textContent).toContain("No filter snapshot is loaded yet"); expect(lookupCalls()).toEqual(["/api/lookup?domain=ads.example&group_id=1"]); // Nothing may read as an answer while the lookup has none. expect(screen.queryByRole("heading", { name: "Blocked" })).toBeNull(); expect(screen.queryByText("Simulating…")).toBeNull(); }); test("a rate limit says how long to wait, from the server's own Retry-After", async () => { lookup = () => json({ error: "rate limited" }, 429, { "retry-after": "12" }); renderPage("/activity/test", { retry: false }); fireEvent.change(await screen.findByLabelText("Domain"), { target: { value: "ads.example" } }); fireEvent.click(screen.getByRole("button", { name: "Simulate" })); const alert = await screen.findByRole("alert"); expect(alert.textContent).toBe("Rate limited. Try again in 12s."); }); test("resubmitting the same domain and group refetches rather than showing a stale verdict", async () => { renderPage(); fireEvent.change(await screen.findByLabelText("Domain"), { target: { value: "ads.example" } }); fireEvent.click(screen.getByRole("button", { name: "Simulate" })); await screen.findByRole("heading", { name: "Blocked" }); expect(lookupCalls()).toHaveLength(1); // The policy can change between two identical questions, so the second one // has to reach the server even though the query key has not moved. lookup = () => json({ ...BLOCKED, blocked: false, reason: "no_match", matched: "", source_url: null }); fireEvent.click(screen.getByRole("button", { name: "Simulate" })); await screen.findByRole("heading", { name: "Allowed" }); expect(lookupCalls()).toEqual([ "/api/lookup?domain=ads.example&group_id=1", "/api/lookup?domain=ads.example&group_id=1", ]); }); test("defaults the group select to the default group (id 1)", async () => { renderPage(); // A RAC Select names its trigger with the current value and then the label, so // the selected group's name is the only thing the trigger shows. const trigger = await screen.findByRole("button", { name: /Group$/ }); expect(trigger.textContent).toContain("default"); }); test("the framing is forward-tense, so it cannot be read as an account of a past query", async () => { renderPage(); await screen.findByRole("heading", { name: "Current policy simulation" }); const intro = screen.getByRole("heading", { name: "Current policy simulation" }).nextElementSibling; expect(intro?.textContent).toContain("would"); expect(intro?.textContent).toContain("right now"); // Nothing on the page may claim to explain a query that already happened. expect(document.body.textContent).not.toContain("Look up"); });