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(); } });});
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();
}
});
});