Spaces:
Sleeping
Sleeping
| 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(<CompareModels />)); | |
| 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(); | |
| }); | |