import React from "react"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { LlmProfileSummary } from "#/api/settings-service/profiles-service.api"; import { ModelMessages } from "#/components/features/chat/model-messages"; import { useModelStore } from "#/stores/model-store"; // Trans needs an i18next instance to render — without one, it emits nothing // in the test env. Render a deterministic string so we can assert on it; the // mocked `cmd` slot wraps `values.name` in a so MonoComponent's // semantics are exercised even though the real component isn't mounted. vi.mock("react-i18next", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en", exists: () => false }, }), Trans: ({ i18nKey, values, components, }: { i18nKey: string; values?: Record; components?: Record; }) => { const cmd = components?.cmd; return ( {`${i18nKey}:`} {cmd ? React.cloneElement(cmd, {}, values?.name ?? "") : values?.name} ); }, }; }); const CONV = "conv-1"; const profile = ( name: string, overrides: Partial = {}, ): LlmProfileSummary => ({ name, model: "anthropic/claude-sonnet-4-6", base_url: null, api_key_set: true, ...overrides, }); describe("", () => { beforeEach(() => { useModelStore.setState({ entriesByConversation: {} }); }); it("renders nothing when there are no entries", () => { const { container } = render( , ); expect(container).toBeEmptyDOMElement(); }); it("renders the empty-profiles hint", () => { useModelStore.getState().show(CONV, null, []); render(); expect(screen.getByText("MODEL$NO_SAVED_PROFILES")).toBeInTheDocument(); expect(screen.getByText("MODEL$NO_PROFILES_HINT")).toBeInTheDocument(); expect( screen.queryByRole("button", { name: /got it/i }), ).not.toBeInTheDocument(); }); it("starts collapsed, reveals profile rows, then model + base_url (no api_key) on row expansion", async () => { useModelStore.getState().show(CONV, null, [ profile("default", { model: "anthropic/claude-sonnet-4-6", base_url: null, }), profile("scratch", { model: "openai/gpt-5", base_url: "https://api.example.com", }), ]); const user = userEvent.setup(); render(); expect(screen.getByText("MODEL$AVAILABLE_PROFILES")).toBeInTheDocument(); // Outer toggle is collapsed: profile rows are not in the document yet. expect( screen.queryByRole("button", { name: /default/i }), ).not.toBeInTheDocument(); await user.click(screen.getByRole("button", { name: /expand/i })); // Profile rows are now visible (as toggles), but their details stay // collapsed until the row is opened. expect( screen.getByRole("button", { name: /default/i }), ).toBeInTheDocument(); expect(screen.queryByText(/model:/i)).toBeNull(); // Opening a row reveals just its model and base_url — never the api_key. await user.click(screen.getByRole("button", { name: /default/i })); expect( screen.getByText(/model:\s+anthropic\/claude-sonnet-4-6/), ).toBeInTheDocument(); expect(screen.getByText(/base_url:/i)).toBeInTheDocument(); expect(screen.queryByText(/api_key/i)).toBeNull(); }); it("only renders entries whose anchor matches the prop", () => { useModelStore.getState().show(CONV, null, [profile("default")]); useModelStore.getState().show(CONV, "evt-1", [profile("scratch")]); // Anchor null → only the first entry is visible. const { unmount } = render( , ); expect(screen.getAllByTestId("model-messages")).toHaveLength(1); expect(screen.getByText("MODEL$AVAILABLE_PROFILES")).toBeInTheDocument(); unmount(); // Anchor "evt-1" → only the second entry is visible (different profile name). render(); expect(screen.getByText("MODEL$AVAILABLE_PROFILES")).toBeInTheDocument(); }); it("does not render entries from other conversations", () => { useModelStore.getState().show("other-conv", null, [profile("default")]); const { container } = render( , ); expect(container).toBeEmptyDOMElement(); }); it("renders a switch entry with the profile name in a ", () => { useModelStore.getState().recordSwitch(CONV, null, "gpt-5"); render(); // Switch branch is taken: the SWITCHED_TO_PROFILE key is rendered, and // none of the list-branch keys are. expect(screen.getByText(/MODEL\$SWITCHED_TO_PROFILE/)).toBeInTheDocument(); expect(screen.queryByText("MODEL$AVAILABLE_PROFILES")).toBeNull(); expect(screen.queryByText("MODEL$NO_SAVED_PROFILES")).toBeNull(); // The profile name renders inside MonoComponent's wrapper. const name = screen.getByText("gpt-5"); expect(name.tagName).toBe("STRONG"); expect(name.className).toContain("font-mono"); }); it("filters switch entries by anchor like list entries", () => { useModelStore.getState().recordSwitch(CONV, "evt-77", "gpt-5"); useModelStore.getState().recordSwitch(CONV, null, "claude-sonnet"); const { unmount } = render( , ); expect(screen.getByText("claude-sonnet")).toBeInTheDocument(); expect(screen.queryByText("gpt-5")).toBeNull(); unmount(); render(); expect(screen.getByText("gpt-5")).toBeInTheDocument(); expect(screen.queryByText("claude-sonnet")).toBeNull(); }); });