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 mockUseResearch = vi.fn(); vi.mock("../../hooks/useResearch", () => ({ useResearch: (...args: unknown[]) => mockUseResearch(...args), })); const configuredResearchSettings = { researchSettings: { enabled: true }, researchGlobalDefaults: { searchProvider: "openrouter", synthesisProvider: "openrouter", synthesisModelId: "gpt-5", maxSourcesPerRun: 20, }, }; const mockFetchSettings = vi.fn().mockResolvedValue(configuredResearchSettings); const mockFetchAuthStatus = vi.fn().mockResolvedValue({ providers: [{ id: "openrouter", type: "api_key", authenticated: false }] }); vi.mock("../../api", () => ({ fetchScripts: vi.fn().mockResolvedValue({}), fetchSettings: (...args: unknown[]) => mockFetchSettings(...args), fetchAuthStatus: (...args: unknown[]) => mockFetchAuthStatus(...args), })); vi.mock("lucide-react", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, Search: () => null, Loader2: ({ className }: { className?: string }) => Loader, }; }); 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", () => { const baseHookValue = { runs: [], selectedRun: null, selectedRunId: null, setSelectedRunId: vi.fn(), availability: { available: true, supportedProviders: ["web-search"], supportedExportFormats: ["markdown", "json", "html"] }, loading: false, error: null, searchQuery: "", setSearchQuery: vi.fn(), createRun: vi.fn(), cancelRun: vi.fn().mockResolvedValue({}), retryRun: vi.fn().mockResolvedValue({}), exportRun: vi.fn().mockResolvedValue({ filename: "run.md", content: "# test", format: "markdown" }), createTaskFromRun: vi.fn().mockResolvedValue({}), attachRunToTask: vi.fn().mockResolvedValue({}), statusCounts: { pending: 0, running: 0, completed: 0, failed: 0, cancelled: 0 }, refresh: vi.fn(), }; beforeEach(() => { vi.clearAllMocks(); mockFetchSettings.mockResolvedValue(configuredResearchSettings); mockFetchAuthStatus.mockResolvedValue({ providers: [{ id: "openrouter", type: "api_key", authenticated: false }] }); mockUseResearch.mockReturnValue(baseHookValue); }); it("shows authentication setup state when required credentials are missing", async () => { mockFetchSettings.mockResolvedValue(configuredResearchSettings); render(); expect(await screen.findByText(/Missing API key for openrouter/i)).toBeInTheDocument(); }); it("renders empty state when required credentials are configured", async () => { mockFetchSettings.mockResolvedValue(configuredResearchSettings); mockFetchAuthStatus.mockResolvedValue({ providers: [{ id: "openrouter", type: "api_key", authenticated: true }] }); render(); expect(await screen.findByTestId("research-state-empty")).toBeInTheDocument(); }); it("renders selected run details", async () => { mockUseResearch.mockReturnValue({ ...baseHookValue, runs: [{ id: "RR-1", title: "t", query: "q", status: "running" }], selectedRun: { id: "RR-1", title: "t", query: "q", status: "running", events: [], results: { summary: "Summary", findings: [], citations: [] } }, selectedRunId: "RR-1", statusCounts: { pending: 0, running: 1, completed: 0, failed: 0, cancelled: 0 }, }); mockFetchAuthStatus.mockResolvedValue({ providers: [{ id: "openrouter", type: "api_key", authenticated: true }] }); render(); expect(await screen.findByTestId("research-state-results")).toHaveTextContent("Summary"); }); it("triggers lifecycle/task/export actions", async () => { const cancelRun = vi.fn().mockResolvedValue({}); const retryRun = vi.fn().mockResolvedValue({}); const createTaskFromRun = vi.fn().mockResolvedValue({}); const attachRunToTask = vi.fn().mockResolvedValue({}); const exportRun = vi.fn().mockResolvedValue({ filename: "run.md", content: "# test", format: "markdown" }); mockUseResearch.mockReturnValue({ ...baseHookValue, runs: [{ id: "RR-1", title: "t", query: "q", status: "pending" }], selectedRun: { id: "RR-1", title: "t", query: "q", status: "pending", events: [{ id: "E-1", message: "queued" }], results: { summary: "Summary", findings: [], citations: [] } }, selectedRunId: "RR-1", cancelRun, retryRun, createTaskFromRun, attachRunToTask, exportRun, }); mockFetchAuthStatus.mockResolvedValue({ providers: [{ id: "openrouter", type: "api_key", authenticated: true }] }); render(); await screen.findByText("Cancel"); fireEvent.click(screen.getByText("Cancel")); fireEvent.click(screen.getByText("Retry")); fireEvent.click(screen.getByText("Create Task")); fireEvent.change(screen.getByPlaceholderText("Task ID"), { target: { value: "FN-1" } }); fireEvent.click(screen.getByText("Attach to Task")); fireEvent.click(screen.getByText("Export MD")); await waitFor(() => { expect(cancelRun).toHaveBeenCalled(); expect(retryRun).toHaveBeenCalled(); expect(createTaskFromRun).toHaveBeenCalled(); expect(attachRunToTask).toHaveBeenCalled(); expect(exportRun).toHaveBeenCalled(); }); }); it("renders unavailable state without interactive workflow controls", async () => { mockUseResearch.mockReturnValue({ ...baseHookValue, availability: { available: false, reason: "disabled" } }); render(); expect(await screen.findByTestId("research-state-unavailable")).toBeInTheDocument(); expect(screen.queryByLabelText("Query")).not.toBeInTheDocument(); expect(screen.queryByText("Create Run")).not.toBeInTheDocument(); }); it("shows setup card when project research is disabled", async () => { mockFetchSettings.mockResolvedValue({ ...configuredResearchSettings, researchSettings: { enabled: false }, }); const onOpenSettings = vi.fn(); render(); expect(await screen.findByText("Research is disabled for this project.")).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "Open Settings" })); expect(onOpenSettings).toHaveBeenCalledWith("research-project"); }); it("shows incomplete defaults setup CTA routed to global research settings", async () => { mockFetchSettings.mockResolvedValue({ researchSettings: { enabled: true }, researchGlobalDefaults: { searchProvider: "tavily", synthesisProvider: undefined, synthesisModelId: undefined, maxSourcesPerRun: 20, }, }); mockFetchAuthStatus.mockResolvedValue({ providers: [{ id: "tavily", type: "api_key", authenticated: true }] }); const onOpenSettings = vi.fn(); render(); expect(await screen.findByText(/Research defaults are incomplete/i)).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "Open Settings" })); expect(onOpenSettings).toHaveBeenCalledWith("research-global"); }); it("shows authentication CTA when provider credentials are missing", async () => { mockFetchSettings.mockResolvedValue({ researchSettings: { enabled: true }, researchGlobalDefaults: { searchProvider: "tavily", synthesisProvider: "openrouter", synthesisModelId: "gpt-5", maxSourcesPerRun: 20, }, }); mockFetchAuthStatus.mockResolvedValue({ providers: [ { id: "tavily", type: "api_key", authenticated: false }, { id: "openrouter", type: "api_key", authenticated: true }, ], }); const onOpenSettings = vi.fn(); render(); expect(await screen.findByText(/Missing API key for tavily/i)).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "Open Settings" })); expect(onOpenSettings).toHaveBeenCalledWith("authentication"); }); it("renders human-readable provider labels", async () => { mockUseResearch.mockReturnValue({ ...baseHookValue, availability: { available: true, supportedProviders: ["web-search", "page-fetch", "llm-synthesis"] }, }); mockFetchAuthStatus.mockResolvedValue({ providers: [{ id: "openrouter", type: "api_key", authenticated: true }] }); render(); expect(await screen.findByText("Web Search")).toBeInTheDocument(); expect(screen.getByText("Page Fetch")).toBeInTheDocument(); expect(screen.getByText("LLM Synthesis")).toBeInTheDocument(); }); it("disables provider checkboxes when sources are disabled in settings", async () => { mockFetchSettings.mockResolvedValue({ ...configuredResearchSettings, researchGlobalDefaults: { ...configuredResearchSettings.researchGlobalDefaults, enabledSources: { webSearch: false, pageFetch: true, github: false, localDocs: true, llmSynthesis: true, }, }, }); mockFetchAuthStatus.mockResolvedValue({ providers: [{ id: "openrouter", type: "api_key", authenticated: true }] }); mockUseResearch.mockReturnValue({ ...baseHookValue, availability: { available: true, supportedProviders: ["web-search", "page-fetch", "llm-synthesis"] }, }); render(); const webSearch = (await screen.findByLabelText("Web Search")) as HTMLInputElement; const pageFetch = screen.getByLabelText("Page Fetch") as HTMLInputElement; expect(webSearch.disabled).toBe(true); expect(pageFetch.disabled).toBe(false); }); it("submits only enabled providers", async () => { const createRun = vi.fn().mockResolvedValue({ run: { id: "RR-2" } }); mockFetchSettings.mockResolvedValue({ ...configuredResearchSettings, researchGlobalDefaults: { ...configuredResearchSettings.researchGlobalDefaults, enabledSources: { webSearch: false, pageFetch: true, github: false, localDocs: false, llmSynthesis: true, }, }, }); mockFetchAuthStatus.mockResolvedValue({ providers: [{ id: "openrouter", type: "api_key", authenticated: true }] }); mockUseResearch.mockReturnValue({ ...baseHookValue, createRun, availability: { available: true, supportedProviders: ["web-search", "page-fetch", "llm-synthesis"] }, }); render(); fireEvent.change(await screen.findByLabelText("Query"), { target: { value: "hello" } }); fireEvent.click(screen.getByText("Create Run")); await waitFor(() => { expect(createRun).toHaveBeenCalledWith(expect.objectContaining({ providers: ["page-fetch", "llm-synthesis"] })); }); }); it("refreshes readiness state when readinessVersion changes", async () => { const onOpenSettings = vi.fn(); mockFetchSettings.mockResolvedValueOnce(configuredResearchSettings); mockFetchAuthStatus.mockResolvedValueOnce({ providers: [{ id: "openrouter", type: "api_key", authenticated: false }] }); const { rerender } = render(); expect(await screen.findByText(/Missing API key for openrouter/i)).toBeInTheDocument(); mockFetchSettings.mockResolvedValueOnce(configuredResearchSettings); mockFetchAuthStatus.mockResolvedValueOnce({ providers: [{ id: "openrouter", type: "api_key", authenticated: true }] }); rerender(); expect(await screen.findByTestId("research-state-empty")).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__layout"); }); });