Files
fusion/packages/dashboard/app/components/__tests__/ResearchView.test.tsx
Fusion c5de7e183b feat(FN-2991): merge fusion/fn-2991
Commits merged:
- fix(FN-2991): align failed research status badge semantics
- feat(FN-2991): add research dashboard view and navigation entry
- fix(FN-2991): complete Step 8 — resolve lint and typecheck gates
- test(FN-2991): address review feedback for Step 7
- test(FN-2991): complete Step 7 — add research store and route tests
- feat(FN-2991): complete Step 6 — add research client API helpers
- feat(FN-2991): complete Step 5 — add research REST routes
- feat(FN-2991): complete Step 4 — wire research store into core exports
- feat(FN-2991): complete Step 3 — implement research store
- feat(FN-2991): complete Step 2 — add research schema and migration
- feat(FN-2991): complete Step 1 — add research domain types

Files changed:
packages/core/src/__tests__/db.test.ts             |  26 +-
 packages/core/src/__tests__/insight-store.test.ts  |   8 +-
 packages/core/src/__tests__/mission-store.test.ts  |   2 +-
 packages/core/src/__tests__/research-store.test.ts | 118 +++++++
 packages/core/src/__tests__/roadmap-store.test.ts  |   2 +-
 packages/core/src/__tests__/run-audit.test.ts      |   2 +-
 packages/core/src/__tests__/task-documents.test.ts |   2 +-
 packages/core/src/db.ts                            |  82 ++++-
 packages/core/src/index.ts                         |  30 ++
 packages/core/src/research-store.ts                | 377 +++++++++++++++++++++
 packages/core/src/research-types.ts                | 162 +++++++++
 packages/core/src/store.ts                         |  14 +
 packages/dashboard/app/App.tsx                     |  12 +
 .../app/api/__tests__/research-api.test.ts         | 120 +++++++
 packages/dashboard/app/api/legacy.ts               | 132 +++++++-
 packages/dashboard/app/components/Header.tsx       |  23 +-
 packages/dashboard/app/components/MobileNavBar.tsx |  16 +-
 packages/dashboard/app/components/ResearchView.css | 110 ++++++
 packages/dashboard/app/components/ResearchView.tsx | 145 ++++++++
 .../app/components/__tests__/Header.test.tsx       |   4 +-
 .../app/components/__tests__/ResearchView.test.tsx | 170 ++++++++++
 packages/dashboard/app/hooks/useViewState.ts       |   3 +-
 packages/dashboard/src/research-routes.ts          | 223 ++++++++++++
 .../src/routes/register-integrated-routers.ts      |   2 +
 24 files changed, 1751 insertions(+), 34 deletions(-)

Fusion-Task-Id: FN-2991
2026-04-29 22:21:27 -07:00

171 lines
5.0 KiB
TypeScript

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(
<Header
onOpenSettings={vi.fn()}
onOpenGitHubImport={vi.fn()}
globalPaused={false}
enginePaused={false}
onToggleGlobalPause={vi.fn()}
onToggleEnginePause={vi.fn()}
view="board"
onChangeView={onChangeView}
/>,
);
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(<ResearchView projectId="p1" />);
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(<ResearchView projectId="p1" />);
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(<ResearchView projectId="p1" />);
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(<ResearchView projectId="p1" />);
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");
});
});