import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { fireEvent, render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { describe, expect, it, vi } from "vitest";
import type { FleetRunSummary } from "../../api/hooks";
import { RunsPage } from "./RunsPage";
/** Match run-list text only, not the timeline's lane labels. */
const LIST_ONLY = { ignore: ".runs-gantt *" };
function run(overrides: Partial<FleetRunSummary> = {}): FleetRunSummary {
return {
routine_id: "r1",
routine_title: "Nightly Audit",
workbench: "nightly-audit-1000",
started_at: Math.floor(Date.now() / 1000) - 10,
started_at_local: "",
finished_at: Math.floor(Date.now() / 1000),
finished_at_local: "",
status: "success",
exit_code: 0,
...overrides,
};
}
function renderPage(runs?: FleetRunSummary[], url = "/runs") {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
if (runs !== undefined) queryClient.setQueryData(["routines", "runs", 100], runs);
render(
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={[url]}>
<RunsPage />
</MemoryRouter>
</QueryClientProvider>,
);
return queryClient;
}
describe("RunsPage", () => {
it("shows a spinner before the query has loaded", () => {
renderPage();
expect(document.querySelector(".spinner")).toBeInTheDocument();
});
it("shows an empty state with no runs at all", () => {
renderPage([]);
expect(screen.getByText("NO RUNS")).toBeInTheDocument();
expect(screen.getByText("no runs yet")).toBeInTheDocument();
});
it("lists fetched runs with routine link and status", () => {
renderPage([
run({ workbench: "wb1", routine_title: "Nightly Audit", status: "success" }),
run({ workbench: "wb2", routine_title: "Weekly Digest", status: "failed", exit_code: 1 }),
]);
expect(screen.getByRole("heading", { name: "Runs" })).toBeInTheDocument();
expect(screen.getByRole("link", { name: "Nightly Audit" })).toHaveAttribute(
"href",
"/routines?history=r1",
);
expect(screen.getByText("Weekly Digest")).toBeInTheDocument();
expect(screen.getByText("Showing 2 of 2 fetched")).toBeInTheDocument();
});
it("links each run to its own deep-linkable detail page", () => {
renderPage([run({ workbench: "wb1", routine_title: "Nightly Audit" })]);
expect(screen.getByRole("link", { name: "VIEW RUN" })).toHaveAttribute("href", "/runs/r1/wb1");
});
it("filters the list by status", () => {
renderPage([
run({ workbench: "wb1", routine_title: "Nightly Audit", status: "success" }),
run({ workbench: "wb2", routine_title: "Weekly Digest", status: "failed" }),
]);
fireEvent.change(screen.getByLabelText("Status filter"), { target: { value: "failed" } });
expect(screen.getByText("Showing 1 of 2 fetched")).toBeInTheDocument();
expect(screen.queryByText("Nightly Audit", LIST_ONLY)).not.toBeInTheDocument();
expect(screen.getByText("Weekly Digest", LIST_ONLY)).toBeInTheDocument();
});
it("filters the list by routine title search", () => {
renderPage([
run({ workbench: "wb1", routine_title: "Nightly Audit" }),
run({ workbench: "wb2", routine_title: "Weekly Digest" }),
]);
fireEvent.change(screen.getByLabelText("Search runs"), { target: { value: "weekly" } });
expect(screen.getByText("Showing 1 of 2 fetched")).toBeInTheDocument();
expect(screen.getByText("Weekly Digest", LIST_ONLY)).toBeInTheDocument();
});
it("shows a no-matches empty state distinct from no-runs-yet", () => {
renderPage([run({ routine_title: "Nightly Audit" })]);
fireEvent.change(screen.getByLabelText("Search runs"), { target: { value: "nonexistent" } });
expect(screen.getByText("NO RUNS")).toBeInTheDocument();
expect(screen.getByText("no runs match the current filters")).toBeInTheDocument();
});
it("offers a load-more action once the fetch limit is hit", () => {
const runs = Array.from({ length: 100 }, (_, i) => run({ workbench: `wb${i}`, started_at: 1_000 + i }));
renderPage(runs);
// getByText, not getByRole: a role query walks the accessibility tree of all 100 rows
// (1.4-2.3s idle), which hit the 5s timeout under load.
expect(screen.getByText("Load more").closest("button")).toBeInTheDocument();
});
it("does not offer load-more under the fetch limit", () => {
renderPage([run()]);
expect(screen.queryByRole("button", { name: "Load more" })).not.toBeInTheDocument();
});
it("narrows the list to a histogram bucket and clears it again", () => {
const now = Math.floor(Date.now() / 1000);
renderPage([
run({ workbench: "wb1", routine_title: "Nightly Audit", started_at: now - 10 }),
run({ workbench: "wb2", routine_title: "Weekly Digest", status: "failed", started_at: now - 3 * 86_400 }),
]);
expect(screen.getByRole("group", { name: "Run activity over time" })).toBeInTheDocument();
const failedBar = screen.getByRole("button", { name: /1 failed/ });
fireEvent.click(failedBar);
expect(failedBar).toHaveAttribute("aria-pressed", "true");
expect(screen.getByText("Showing 1 of 2 fetched")).toBeInTheDocument();
expect(screen.queryByText("Nightly Audit", LIST_ONLY)).not.toBeInTheDocument();
fireEvent.click(failedBar);
expect(screen.getByText("Showing 2 of 2 fetched")).toBeInTheDocument();
fireEvent.click(failedBar);
fireEvent.click(screen.getByRole("button", { name: /Clear time selection/ }));
expect(screen.getByText("Showing 2 of 2 fetched")).toBeInTheDocument();
});
it("resets the bucket selection when the time window changes", () => {
const now = Math.floor(Date.now() / 1000);
renderPage([run({ workbench: "wb1", started_at: now - 10 })]);
fireEvent.click(screen.getByRole("button", { name: /1 success/ }));
fireEvent.change(screen.getByLabelText("Time range filter"), { target: { value: "24h" } });
expect(screen.queryByRole("button", { name: /Clear time selection/ })).not.toBeInTheDocument();
});
it("restores filters and a histogram range from the URL", () => {
const now = Math.floor(Date.now() / 1000);
renderPage(
[
run({ workbench: "wb1", routine_title: "Nightly Audit", status: "failed", started_at: now - 10 }),
run({ workbench: "wb2", routine_title: "Weekly Digest", status: "failed", started_at: now - 10 }),
run({ workbench: "wb3", routine_title: "Weekly Report", status: "success", started_at: now - 10 }),
],
`/runs?q=weekly&status=failed&when=24h&from=${now - 60}&to=${now}`,
);
expect(screen.getByLabelText("Search runs")).toHaveValue("weekly");
expect(screen.getByLabelText("Status filter")).toHaveValue("failed");
expect(screen.getByLabelText("Time range filter")).toHaveValue("24h");
expect(screen.getByRole("button", { name: /Clear time selection/ })).toBeInTheDocument();
expect(screen.getByText("Showing 1 of 3 fetched")).toBeInTheDocument();
});
it("exports the shown runs as a CSV download", () => {
const createObjectURL = vi.fn((blob: Blob) => (blob.type === "text/csv;charset=utf-8" ? "blob:runs" : "blob:wrong"));
const revokeObjectURL = vi.fn();
const origCreate = URL.createObjectURL;
const origRevoke = URL.revokeObjectURL;
URL.createObjectURL = createObjectURL;
URL.revokeObjectURL = revokeObjectURL;
const click = vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => {});
try {
renderPage([run({ workbench: "wb1" })]);
fireEvent.click(screen.getByRole("button", { name: "Export CSV" }));
expect(createObjectURL).toHaveBeenCalledOnce();
expect(click).toHaveBeenCalledOnce();
expect(revokeObjectURL).toHaveBeenCalledWith("blob:runs");
} finally {
click.mockRestore();
URL.createObjectURL = origCreate;
URL.revokeObjectURL = origRevoke;
}
});
it("disables CSV export when no runs are shown", () => {
renderPage([]);
expect(screen.getByRole("button", { name: "Export CSV" })).toBeDisabled();
});
});