Files
fusion/packages/dashboard/app/__tests__/insight-model-selector.test.tsx
Timothy Laurent f7080fef6a feat(insights): add collapsible model selector to InsightsView with gear trigger
Adds a Settings gear button to the InsightsView action bar that toggles a
collapsible model configuration row. The row contains a CustomModelDropdown
allowing users to override the insight generation model. Selections persist
to localStorage under the "fusion-insight-model" key. A yellow indicator dot
appears on the gear when a non-default model is active.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 14:09:39 -07:00

147 lines
5.3 KiB
TypeScript

// @vitest-environment jsdom
/**
* Insight Model Selector — TDD Red Phase
*
* These tests define the contract for the collapsible model-selector gear button
* on InsightsView. They are expected to FAIL until the UI is implemented (Task 4).
*
* Contract:
* 1. Gear icon button (data-testid="toggle-model-config") toggles a config row.
* 2. Config row (data-testid="model-config") is hidden by default.
* 3. CustomModelDropdown (data-testid="model-dropdown") appears inside the row.
* 4. Selected model persists to localStorage key "fusion-insight-model".
* 5. On mount, the stored model is restored into the dropdown.
* 6. A yellow indicator dot (class "insights-model-indicator") appears on the gear
* when a non-default model is selected.
*/
import { describe, it, expect, vi, beforeEach, beforeAll, afterEach } from "vitest";
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
import * as jestDomMatchers from "@testing-library/jest-dom/matchers";
import { InsightsView } from "../components/InsightsView";
// Register jest-dom matchers (setup files not running in this environment)
expect.extend(jestDomMatchers);
// Ensure localStorage is available (jsdom in this environment may not provide it)
const localStorageStore: Record<string, string> = {};
beforeAll(() => {
if (typeof localStorage === "undefined" || typeof localStorage.clear !== "function") {
const mock = {
getItem: (key: string) => localStorageStore[key] ?? null,
setItem: (key: string, value: string) => {
localStorageStore[key] = value;
},
removeItem: (key: string) => {
delete localStorageStore[key];
},
clear: () => {
Object.keys(localStorageStore).forEach((k) => delete localStorageStore[k]);
},
get length() {
return Object.keys(localStorageStore).length;
},
key: (index: number) => Object.keys(localStorageStore)[index] ?? null,
};
Object.defineProperty(globalThis, "localStorage", { value: mock, writable: true });
}
});
// Mock useInsights hook
vi.mock("../hooks/useInsights", () => ({
useInsights: () => ({
sections: [],
loading: false,
error: null,
latestRun: null,
isRunInFlight: false,
runError: null,
refresh: vi.fn(),
runInsights: vi.fn(),
dismiss: vi.fn(),
createTask: vi.fn(),
archive: vi.fn(),
unarchive: vi.fn(),
toggleShowArchived: vi.fn(),
dismissStates: new Map(),
createTaskStates: new Map(),
archiveStates: new Map(),
unarchiveStates: new Map(),
totalCount: 0,
dismissedCount: 0,
archivedCount: 0,
showArchived: false,
}),
}));
// Mock CustomModelDropdown since it has complex portal behavior
vi.mock("../components/CustomModelDropdown", () => ({
CustomModelDropdown: ({ value, onChange, placeholder }: any) => (
<div data-testid="model-dropdown">
<span data-testid="model-value">{value || placeholder}</span>
<button data-testid="model-change" onClick={() => onChange("openai/gpt-4o")} />
</div>
),
}));
const mockAddToast = vi.fn();
describe("Insight model selector", () => {
beforeEach(() => {
localStorage.clear();
});
afterEach(() => {
cleanup();
});
it("renders a gear button to toggle model config", () => {
render(<InsightsView addToast={mockAddToast} />);
expect(screen.getByTestId("toggle-model-config")).toBeInTheDocument();
});
it("does not show model config row by default", () => {
render(<InsightsView addToast={mockAddToast} />);
expect(screen.queryByTestId("model-config")).not.toBeInTheDocument();
});
it("shows model config row when gear is clicked", () => {
render(<InsightsView addToast={mockAddToast} />);
fireEvent.click(screen.getByTestId("toggle-model-config"));
expect(screen.getByTestId("model-config")).toBeInTheDocument();
expect(screen.getByTestId("model-dropdown")).toBeInTheDocument();
});
it("passes models prop to CustomModelDropdown", () => {
const models = [
{ id: "gpt-4o", provider: "openai", name: "GPT-4o" },
];
render(<InsightsView addToast={mockAddToast} models={models as any} />);
fireEvent.click(screen.getByTestId("toggle-model-config"));
expect(screen.getByTestId("model-dropdown")).toBeInTheDocument();
});
it("persists selected model to localStorage", () => {
render(<InsightsView addToast={mockAddToast} />);
fireEvent.click(screen.getByTestId("toggle-model-config"));
fireEvent.click(screen.getByTestId("model-change"));
expect(localStorage.getItem("fusion-insight-model")).toBe("openai/gpt-4o");
});
it("restores model from localStorage on mount", () => {
localStorage.setItem("fusion-insight-model", "anthropic/claude-sonnet-4-5");
render(<InsightsView addToast={mockAddToast} />);
fireEvent.click(screen.getByTestId("toggle-model-config"));
expect(screen.getByTestId("model-value")).toHaveTextContent("anthropic/claude-sonnet-4-5");
});
it("shows indicator dot on gear when a model is selected", () => {
render(<InsightsView addToast={mockAddToast} />);
fireEvent.click(screen.getByTestId("toggle-model-config"));
fireEvent.click(screen.getByTestId("model-change"));
expect(
screen.getByTestId("toggle-model-config").querySelector(".insights-model-indicator"),
).toBeInTheDocument();
});
});