317 lines · 9.1 KB · TypeScript
RawDownload
import { type ProviderKind, type ServerProvider } from "@t3tools/contracts";import { page } from "vitest/browser";import { afterEach, describe, expect, it, vi } from "vitest";import { render } from "vitest-browser-react"; import { ProviderModelPicker } from "./ProviderModelPicker";import { getCustomModelOptionsByProvider } from "../../modelSelection";import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings"; function effort(value: string, isDefault = false) {  return {    value,    label: value,    ...(isDefault ? { isDefault: true } : {}),  };} const TEST_PROVIDERS: ReadonlyArray<ServerProvider> = [  {    provider: "codex",    enabled: true,    installed: true,    version: "0.116.0",    status: "ready",    authStatus: "authenticated",    checkedAt: new Date().toISOString(),    models: [      {        slug: "gpt-5-codex",        name: "GPT-5 Codex",        isCustom: false,        capabilities: {          reasoningEffortLevels: [effort("low"), effort("medium", true), effort("high")],          supportsFastMode: true,          supportsThinkingToggle: false,          contextWindowOptions: [],          promptInjectedEffortLevels: [],        },      },      {        slug: "gpt-5.3-codex",        name: "GPT-5.3 Codex",        isCustom: false,        capabilities: {          reasoningEffortLevels: [effort("low"), effort("medium", true), effort("high")],          supportsFastMode: true,          supportsThinkingToggle: false,          contextWindowOptions: [],          promptInjectedEffortLevels: [],        },      },    ],  },  {    provider: "claudeAgent",    enabled: true,    installed: true,    version: "1.0.0",    status: "ready",    authStatus: "authenticated",    checkedAt: new Date().toISOString(),    models: [      {        slug: "claude-opus-4-6",        name: "Claude Opus 4.6",        isCustom: false,        capabilities: {          reasoningEffortLevels: [            effort("low"),            effort("medium", true),            effort("high"),            effort("max"),          ],          supportsFastMode: false,          supportsThinkingToggle: true,          contextWindowOptions: [],          promptInjectedEffortLevels: [],        },      },      {        slug: "claude-sonnet-4-6",        name: "Claude Sonnet 4.6",        isCustom: false,        capabilities: {          reasoningEffortLevels: [            effort("low"),            effort("medium", true),            effort("high"),            effort("max"),          ],          supportsFastMode: false,          supportsThinkingToggle: true,          contextWindowOptions: [],          promptInjectedEffortLevels: [],        },      },      {        slug: "claude-haiku-4-5",        name: "Claude Haiku 4.5",        isCustom: false,        capabilities: {          reasoningEffortLevels: [effort("low"), effort("medium", true), effort("high")],          supportsFastMode: false,          supportsThinkingToggle: true,          contextWindowOptions: [],          promptInjectedEffortLevels: [],        },      },    ],  },]; async function mountPicker(props: {  provider: ProviderKind;  model: string;  lockedProvider: ProviderKind | null;  providers?: ReadonlyArray<ServerProvider>;  triggerVariant?: "ghost" | "outline";}) {  const host = document.createElement("div");  document.body.append(host);  const onProviderModelChange = vi.fn();  const providers = props.providers ?? TEST_PROVIDERS;  const modelOptionsByProvider = getCustomModelOptionsByProvider(    DEFAULT_UNIFIED_SETTINGS,    providers,    props.provider,    props.model,  );  const screen = await render(    <ProviderModelPicker      provider={props.provider}      model={props.model}      lockedProvider={props.lockedProvider}      providers={providers}      modelOptionsByProvider={modelOptionsByProvider}      triggerVariant={props.triggerVariant}      onProviderModelChange={onProviderModelChange}    />,    { container: host },  );   return {    onProviderModelChange,    cleanup: async () => {      await screen.unmount();      host.remove();    },  };} describe("ProviderModelPicker", () => {  afterEach(() => {    document.body.innerHTML = "";  });   it("shows provider submenus when provider switching is allowed", async () => {    const mounted = await mountPicker({      provider: "claudeAgent",      model: "claude-opus-4-6",      lockedProvider: null,    });     try {      await page.getByRole("button").click();       await vi.waitFor(() => {        const text = document.body.textContent ?? "";        expect(text).toContain("Codex");        expect(text).toContain("Claude");        expect(text).not.toContain("Claude Sonnet 4.6");      });    } finally {      await mounted.cleanup();    }  });   it("opens provider submenus with a visible gap from the parent menu", async () => {    const mounted = await mountPicker({      provider: "claudeAgent",      model: "claude-opus-4-6",      lockedProvider: null,    });     try {      await page.getByRole("button").click();      const providerTrigger = page.getByRole("menuitem", { name: "Codex" });      await providerTrigger.hover();       await vi.waitFor(() => {        expect(document.body.textContent ?? "").toContain("GPT-5 Codex");      });       const providerTriggerElement = Array.from(        document.querySelectorAll<HTMLElement>('[role="menuitem"]'),      ).find((element) => element.textContent?.includes("Codex"));      if (!providerTriggerElement) {        throw new Error("Expected the Codex provider trigger to be mounted.");      }       const providerTriggerRect = providerTriggerElement.getBoundingClientRect();      const modelElement = Array.from(        document.querySelectorAll<HTMLElement>('[role="menuitemradio"]'),      ).find((element) => element.textContent?.includes("GPT-5 Codex"));      if (!modelElement) {        throw new Error("Expected the submenu model option to be mounted.");      }       const submenuPopup = modelElement.closest('[data-slot="menu-sub-content"]');      if (!(submenuPopup instanceof HTMLElement)) {        throw new Error("Expected submenu popup to be mounted.");      }       const submenuRect = submenuPopup.getBoundingClientRect();       expect(submenuRect.left).toBeGreaterThanOrEqual(providerTriggerRect.right);      expect(submenuRect.left - providerTriggerRect.right).toBeGreaterThanOrEqual(2);    } finally {      await mounted.cleanup();    }  });   it("shows models directly when the provider is locked mid-thread", async () => {    const mounted = await mountPicker({      provider: "claudeAgent",      model: "claude-opus-4-6",      lockedProvider: "claudeAgent",    });     try {      await page.getByRole("button").click();       await vi.waitFor(() => {        const text = document.body.textContent ?? "";        expect(text).toContain("Claude Sonnet 4.6");        expect(text).toContain("Claude Haiku 4.5");        expect(text).not.toContain("Codex");      });    } finally {      await mounted.cleanup();    }  });   it("dispatches the canonical slug when a model is selected", async () => {    const mounted = await mountPicker({      provider: "claudeAgent",      model: "claude-opus-4-6",      lockedProvider: "claudeAgent",    });     try {      await page.getByRole("button").click();      await page.getByRole("menuitemradio", { name: "Claude Sonnet 4.6" }).click();       expect(mounted.onProviderModelChange).toHaveBeenCalledWith(        "claudeAgent",        "claude-sonnet-4-6",      );    } finally {      await mounted.cleanup();    }  });   it("shows disabled providers as non-selectable entries", async () => {    const disabledProviders = TEST_PROVIDERS.slice();    const claudeIndex = disabledProviders.findIndex(      (provider) => provider.provider === "claudeAgent",    );    if (claudeIndex >= 0) {      const claudeProvider = disabledProviders[claudeIndex]!;      disabledProviders[claudeIndex] = {        ...claudeProvider,        enabled: false,        status: "disabled",      };    }    const mounted = await mountPicker({      provider: "codex",      model: "gpt-5-codex",      lockedProvider: null,      providers: disabledProviders,    });     try {      await page.getByRole("button").click();       await vi.waitFor(() => {        const text = document.body.textContent ?? "";        expect(text).toContain("Claude");        expect(text).toContain("Disabled");        expect(text).not.toContain("Claude Sonnet 4.6");      });    } finally {      await mounted.cleanup();    }  });   it("accepts outline trigger styling", async () => {    const mounted = await mountPicker({      provider: "codex",      model: "gpt-5-codex",      lockedProvider: null,      triggerVariant: "outline",    });     try {      const button = document.querySelector("button");      if (!(button instanceof HTMLButtonElement)) {        throw new Error("Expected picker trigger button to be rendered.");      }      expect(button.className).toContain("border-input");      expect(button.className).toContain("bg-popover");    } finally {      await mounted.cleanup();    }  });});