import { expect, it, vi } from "vitest"; import { act, render, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import CompareModels from "./CompareModels"; import { compareModels, getModels } from "../api"; // CompareModels suspends on first render while the registry promise settles, // so React 19 requires the initial render inside an awaited act(). const renderCompare = () => act(async () => render()); vi.mock("../api", () => ({ compareModels: vi.fn(), getModels: vi.fn() })); const registry = { models: [ { id: "twitter-roberta", name: "cardiffnlp/twitter-roberta-base-sentiment-latest", task: "sentiment", labels: ["negative", "neutral", "positive"], domain: "social / short English text", note: "", default: true, loaded: true, }, { id: "distilbert-sst2", name: "distilbert-base-uncased-finetuned-sst-2-english", task: "sentiment", labels: ["negative", "positive"], domain: "general binary sentiment", note: "", default: true, loaded: true, }, { id: "finbert", name: "ProsusAI/finbert", task: "sentiment", labels: ["negative", "neutral", "positive"], domain: "financial text", note: "Useful for finance/news sentences; misleading outside that domain.", default: false, loaded: false, }, ], }; it("checks the two default models and leaves lazy optional models off with a load note", async () => { vi.mocked(getModels).mockResolvedValue(registry); await renderCompare(); expect(await screen.findByRole("checkbox", { name: /twitter-roberta/ })).toBeChecked(); expect(screen.getByRole("checkbox", { name: /distilbert-sst2/ })).toBeChecked(); expect(screen.getByRole("checkbox", { name: /finbert/ })).not.toBeChecked(); // Optional models are lazy-loaded server-side; the UI must warn about the // one-time load cost before the user opts in. expect(screen.getByText(/loads on first use/i)).toBeInTheDocument(); expect(getModels).toHaveBeenCalledWith("sentiment"); expect(compareModels).not.toHaveBeenCalled(); }); it("compares the selected models and renders one readout per model with its own score keys", async () => { vi.mocked(getModels).mockResolvedValue(registry); vi.mocked(compareModels).mockResolvedValue({ results: [ { model_id: "twitter-roberta", name: "cardiffnlp/twitter-roberta-base-sentiment-latest", domain: "social / short English text", label: "positive", scores: { negative: 0.03, neutral: 0.17, positive: 0.8 }, confidence: 0.8, latency_ms: 42.6, note: "", }, { model_id: "distilbert-sst2", name: "distilbert-base-uncased-finetuned-sst-2-english", domain: "general binary sentiment", label: "negative", scores: { negative: 0.61, positive: 0.39 }, confidence: 0.61, latency_ms: 17.2, note: "Binary model: it has no neutral class to fall back on.", }, ], }); const user = userEvent.setup(); await renderCompare(); await user.click(await screen.findByRole("button", { name: "Compare models" })); const roberta = await screen.findByRole("group", { name: "twitter-roberta" }); const distilbert = screen.getByRole("group", { name: "distilbert-sst2" }); // Each readout shows its prediction (badge + score bar), confidence // (readout + winning bar), and latency. expect(within(roberta).getAllByText("positive")).toHaveLength(2); expect(within(roberta).getAllByText("80.0%")).toHaveLength(2); expect(within(roberta).getByText("43 ms")).toBeInTheDocument(); expect(within(distilbert).getAllByText("negative")).toHaveLength(2); expect(within(distilbert).getByText("17 ms")).toBeInTheDocument(); expect(within(distilbert).getByText(/no neutral class/)).toBeInTheDocument(); // Dynamic scores: the 2-class model renders only its own keys. expect(within(distilbert).queryByText("neutral")).not.toBeInTheDocument(); expect(within(roberta).getByText("neutral")).toBeInTheDocument(); // The default textarea text and the default selection go to the API as-is. expect(compareModels).toHaveBeenCalledWith("Our quarterly revenue outlook improved", [ "twitter-roberta", "distilbert-sst2", ]); }); it("includes an opted-in lazy model in the request", async () => { vi.mocked(getModels).mockResolvedValue(registry); vi.mocked(compareModels).mockResolvedValue({ results: [] }); const user = userEvent.setup(); await renderCompare(); await user.click(await screen.findByRole("checkbox", { name: /finbert/ })); await user.click(screen.getByRole("button", { name: "Compare models" })); expect(compareModels).toHaveBeenCalledWith("Our quarterly revenue outlook improved", [ "twitter-roberta", "distilbert-sst2", "finbert", ]); }); it("surfaces the API error detail when the comparison fails", async () => { vi.mocked(getModels).mockResolvedValue(registry); vi.mocked(compareModels).mockRejectedValue(new Error("Unknown model id: finbert")); const user = userEvent.setup(); await renderCompare(); await user.click(await screen.findByRole("button", { name: "Compare models" })); expect(await screen.findByText("Unknown model id: finbert")).toBeInTheDocument(); });