/** * @file QueryExplorer.test.tsx * @description The Query Explorer page: it renders a page of rows with session * links, sorts by an allowed column on a header click (flipping on a second), * switches entity, and points both export links at the current filter. * @author Nguyễn Ngọc Trí Vĩ */ import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { MemoryRouter } from "react-router-dom"; import { beforeEach, describe, expect, it, vi } from "vitest"; import i18n from "i18next"; const { query } = vi.hoisted(() => ({ query: { page: vi.fn(), exportUrl: (p: URLSearchParams, f: string) => `/api/query?${p.toString()}&format=${f}`, }, })); vi.mock("../../lib/api", () => ({ api: { query } })); vi.mock("../../hooks/useAiTools", () => ({ useAiTools: () => ({ tools: [{ id: "codex", label: "Codex" }] }), })); import { QueryExplorer } from "../QueryExplorer"; const page = (entity: string, rows: Record[]) => ({ entity, columns: Object.keys(rows[0] ?? {}), sortable: entity === "sessions" ? ["started_at", "name"] : ["created_at"], statuses: [], rows, total: rows.length, limit: 50, offset: 0, export_cap: 50000, }); const lastParams = () => { const calls = query.page.mock.calls; return calls[calls.length - 1]![0] as URLSearchParams; }; beforeEach(() => { i18n.changeLanguage("en"); vi.clearAllMocks(); query.page.mockImplementation((p: URLSearchParams) => Promise.resolve( p.get("entity") === "events" ? page("events", [{ id: "1", session_id: "sess-abcdef12", created_at: "2026-10-01" }]) : page("sessions", [{ id: "sess-abcdef12", name: "first", started_at: "2026-10-01" }]) ) ); }); describe("QueryExplorer", () => { it("shows rows, sorts by an allowed column and exports the filter", async () => { const user = userEvent.setup(); render( ); expect(await screen.findByText("first")).toBeInTheDocument(); expect(screen.getByRole("link", { name: "sess-abc" })).toHaveAttribute( "href", "/sessions/sess-abcdef12" ); await user.click(screen.getByRole("button", { name: "name" })); await waitFor(() => expect(lastParams().get("sort")).toBe("name")); expect(lastParams().get("dir")).toBe("desc"); await user.click(screen.getByRole("button", { name: "name" })); await waitFor(() => expect(lastParams().get("dir")).toBe("asc")); // A column outside `sortable` has no sort button. expect(screen.queryByRole("button", { name: "id" })).toBeNull(); const csv = screen.getByRole("link", { name: /Export CSV/ }); expect(csv.getAttribute("href")).toContain("sort=name"); expect(csv.getAttribute("href")).toContain("format=csv"); }); it("switches entity and drops the status filter for events", async () => { const user = userEvent.setup(); render( ); await screen.findByText("first"); expect(screen.getByRole("button", { name: /Status/ })).toBeInTheDocument(); await user.click(screen.getByRole("button", { name: /^Data/ })); await user.click(screen.getByRole("option", { name: "Events" })); await waitFor(() => expect(lastParams().get("entity")).toBe("events")); expect(screen.queryByRole("button", { name: /Status/ })).toBeNull(); }); });