import { describe, it, expect, vi, beforeEach } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { Header } from "../Header";
import { ResearchView } from "../ResearchView";
const mockListResearchRuns = vi.fn();
const mockGetResearchStats = vi.fn();
vi.mock("../../api", () => ({
fetchScripts: vi.fn().mockResolvedValue({}),
listResearchRuns: (...args: unknown[]) => mockListResearchRuns(...args),
getResearchStats: (...args: unknown[]) => mockGetResearchStats(...args),
}));
function mockMatchMediaDesktop() {
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
onchange: null,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
}
describe("Research navigation", () => {
it("shows research in header overflow and activates view change", async () => {
mockMatchMediaDesktop();
const onChangeView = vi.fn();
render(
,
);
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
await waitFor(() => {
expect(screen.getByTestId("view-overflow-research")).toBeInTheDocument();
});
fireEvent.click(screen.getByTestId("view-overflow-research"));
expect(onChangeView).toHaveBeenCalledWith("research");
});
});
describe("ResearchView", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders empty state", async () => {
mockListResearchRuns.mockResolvedValue({ runs: [] });
mockGetResearchStats.mockResolvedValue({
total: 0,
byStatus: { pending: 0, running: 0, completed: 0, failed: 0, cancelled: 0 },
});
render();
await waitFor(() => {
expect(screen.getByTestId("research-state-empty")).toBeInTheDocument();
});
});
it("renders loading and then running/results states", async () => {
mockListResearchRuns.mockResolvedValue({
runs: [
{
id: "RR-1",
query: "evaluate release automation",
topic: "Release automation",
status: "running",
providerConfig: {},
sources: [],
events: [],
results: { summary: "Initial synthesis complete", findings: [], citations: [], synthesizedOutput: "" },
error: null,
tokenUsage: null,
tags: [],
metadata: null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
startedAt: null,
completedAt: null,
cancelledAt: null,
},
],
});
mockGetResearchStats.mockResolvedValue({
total: 1,
byStatus: { pending: 0, running: 1, completed: 0, failed: 0, cancelled: 0 },
});
render();
expect(screen.getByTestId("research-state-loading")).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByTestId("research-state-running")).toBeInTheDocument();
expect(screen.getByTestId("research-state-results")).toBeInTheDocument();
});
});
it("uses failure badge treatment for failed runs", async () => {
mockListResearchRuns.mockResolvedValue({
runs: [
{
id: "RR-2",
query: "evaluate failed orchestration",
topic: "Failure case",
status: "failed",
providerConfig: {},
sources: [],
events: [],
results: null,
error: "provider timeout",
tokenUsage: null,
tags: [],
metadata: null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
startedAt: null,
completedAt: null,
cancelledAt: null,
},
],
});
mockGetResearchStats.mockResolvedValue({
total: 1,
byStatus: { pending: 0, running: 0, completed: 0, failed: 1, cancelled: 0 },
});
render();
await waitFor(() => {
expect(screen.getByText("Failed")).toHaveClass("research-view__status-badge--failed");
});
});
it("renders error state when fetch fails", async () => {
mockListResearchRuns.mockRejectedValue(new Error("boom"));
mockGetResearchStats.mockResolvedValue({
total: 0,
byStatus: { pending: 0, running: 0, completed: 0, failed: 0, cancelled: 0 },
});
render();
await waitFor(() => {
expect(screen.getByTestId("research-state-error")).toBeInTheDocument();
});
});
it("includes mobile layout media rule", async () => {
const css = await import("../ResearchView.css?inline");
expect(css.default).toContain("@media (max-width: 768px)");
expect(css.default).toContain(".research-view__stats");
});
});