chore(cli): remove unused BaseUrlSelect/ApiKeySelect/ManualConfigModal (plan 14)

Components were created by F4 per master-plan §3.7-§3.9 but never integrated:
the `*ToolCard.tsx` files use the legacy `ManualConfigModal` from
`@/shared/components` (barrel-export root), not the F4 versions in
`@/shared/components/cli`. The 3 files were sitting as dead code.

Removes:
- src/shared/components/cli/BaseUrlSelect.tsx
- src/shared/components/cli/ApiKeySelect.tsx
- src/shared/components/cli/ManualConfigModal.tsx
- tests/unit/ui/BaseUrlSelect.test.tsx
- tests/unit/ui/ApiKeySelect.test.tsx
- tests/unit/ui/ManualConfigModal.test.tsx

Updates `src/shared/components/cli/index.ts` to drop the dead exports.

Kept (actively used by page clients):
- CliToolCard, CliConceptCard, CliComparisonCard

Verified:
- typecheck:core + noimplicit:core clean
- npx eslint src/shared/components/cli/: 0 issues
- check:cycles: clean (212 files, -3 from removed components)
- 50/50 UI tests pass across the 3 kept components + 3 page clients
- 0 residual imports of the 3 removed symbols anywhere in src/ or tests/

Closes code review v3 gap #1 (dead code).
This commit is contained in:
diegosouzapw
2026-05-28 15:57:02 -03:00
parent 27d4a7aeac
commit dfa17ef621
7 changed files with 0 additions and 919 deletions

View File

@@ -1,99 +0,0 @@
"use client";
import { useState } from "react";
import { cn } from "@/shared/utils/cn";
export interface ApiKeyEntry {
id: string;
name: string;
prefix?: string;
createdAt?: string;
}
export interface ApiKeySelectProps {
keys: ApiKeyEntry[];
value: string;
onChange: (value: string) => void;
label?: string;
disabled?: boolean;
}
const MANUAL_VALUE = "__manual__";
export default function ApiKeySelect({
keys,
value,
onChange,
label = "API Key",
disabled = false,
}: ApiKeySelectProps) {
const isManual = !keys.some((k) => k.id === value) && value !== "";
const [showManual, setShowManual] = useState<boolean>(isManual);
const [manualValue, setManualValue] = useState<string>(isManual ? value : "");
function handleSelectChange(e: React.ChangeEvent<HTMLSelectElement>) {
const val = e.target.value;
if (val === MANUAL_VALUE) {
setShowManual(true);
onChange(manualValue);
} else {
setShowManual(false);
onChange(val);
}
}
function handleManualInput(e: React.ChangeEvent<HTMLInputElement>) {
const val = e.target.value;
setManualValue(val);
onChange(val);
}
const selectValue = showManual ? MANUAL_VALUE : (value || "");
return (
<div className="flex flex-col gap-1.5">
{label && (
<label className="text-sm font-medium text-text-main">{label}</label>
)}
<select
value={selectValue}
onChange={handleSelectChange}
disabled={disabled}
className={cn(
"w-full px-3 py-2 rounded-lg text-sm",
"bg-white dark:bg-white/5 border border-black/10 dark:border-white/10",
"text-text-main focus:outline-none focus:border-primary/50",
"disabled:opacity-50 disabled:cursor-not-allowed"
)}
>
{keys.length === 0 && !showManual && (
<option value="" disabled>
Nenhuma API key cadastrada
</option>
)}
{keys.map((k) => (
<option key={k.id} value={k.id}>
{k.name}
{k.prefix ? ` (${k.prefix}…)` : ""}
</option>
))}
<option value={MANUAL_VALUE}>Inserir manualmente</option>
</select>
{showManual && (
<input
type="text"
value={manualValue}
onChange={handleManualInput}
disabled={disabled}
placeholder="sk-…"
className={cn(
"w-full px-3 py-2 rounded-lg text-sm font-mono",
"bg-white dark:bg-white/5 border border-black/10 dark:border-white/10",
"text-text-main focus:outline-none focus:border-primary/50",
"disabled:opacity-50 disabled:cursor-not-allowed"
)}
/>
)}
</div>
);
}

View File

@@ -1,102 +0,0 @@
"use client";
import { useState } from "react";
import { cn } from "@/shared/utils/cn";
export interface BaseUrlSelectProps {
value: string;
onChange: (value: string) => void;
cloudEnabled: boolean;
cloudUrl?: string;
label?: string;
disabled?: boolean;
}
type OptionKey = "local" | "cloud" | "custom";
function getDefaultLocal(): string {
if (typeof window !== "undefined") {
return window.location.origin;
}
return "http://localhost:20128";
}
export default function BaseUrlSelect({
value,
onChange,
cloudEnabled,
cloudUrl,
label = "Base URL",
disabled = false,
}: BaseUrlSelectProps) {
const localUrl = getDefaultLocal();
function detectOption(val: string): OptionKey {
if (val === localUrl) return "local";
if (cloudEnabled && cloudUrl && val === cloudUrl) return "cloud";
return "custom";
}
const [selectedOption, setSelectedOption] = useState<OptionKey>(() => detectOption(value));
const [customValue, setCustomValue] = useState<string>(
detectOption(value) === "custom" ? value : ""
);
function handleSelectChange(e: React.ChangeEvent<HTMLSelectElement>) {
const opt = e.target.value as OptionKey;
setSelectedOption(opt);
if (opt === "local") {
onChange(localUrl);
} else if (opt === "cloud" && cloudUrl) {
onChange(cloudUrl);
} else if (opt === "custom") {
onChange(customValue);
}
}
function handleCustomInput(e: React.ChangeEvent<HTMLInputElement>) {
const val = e.target.value;
setCustomValue(val);
onChange(val);
}
return (
<div className="flex flex-col gap-1.5">
{label && (
<label className="text-sm font-medium text-text-main">{label}</label>
)}
<select
value={selectedOption}
onChange={handleSelectChange}
disabled={disabled}
className={cn(
"w-full px-3 py-2 rounded-lg text-sm",
"bg-white dark:bg-white/5 border border-black/10 dark:border-white/10",
"text-text-main focus:outline-none focus:border-primary/50",
"disabled:opacity-50 disabled:cursor-not-allowed"
)}
>
<option value="local">Local ({localUrl})</option>
{cloudEnabled && cloudUrl && (
<option value="cloud">Cloud ({cloudUrl})</option>
)}
<option value="custom">Custom</option>
</select>
{selectedOption === "custom" && (
<input
type="text"
value={customValue}
onChange={handleCustomInput}
disabled={disabled}
placeholder="https://…"
className={cn(
"w-full px-3 py-2 rounded-lg text-sm font-mono",
"bg-white dark:bg-white/5 border border-black/10 dark:border-white/10",
"text-text-main focus:outline-none focus:border-primary/50",
"disabled:opacity-50 disabled:cursor-not-allowed"
)}
/>
)}
</div>
);
}

View File

@@ -1,152 +0,0 @@
"use client";
import { useState } from "react";
import { cn } from "@/shared/utils/cn";
import type { CliCatalogEntry } from "@/shared/schemas/cliCatalog";
export interface ManualConfigModalCustomCode {
language: string;
code: string;
}
export interface ManualConfigModalProps {
open: boolean;
onClose: () => void;
tool: CliCatalogEntry;
baseUrl: string;
apiKey: string;
model: string;
customCode?: ManualConfigModalCustomCode;
}
function interpolate(template: string, vars: Record<string, string>): string {
return template
.replace(/\{\{baseUrl\}\}/g, vars["baseUrl"] ?? "")
.replace(/\{\{apiKey\}\}/g, vars["apiKey"] ?? "")
.replace(/\{\{model\}\}/g, vars["model"] ?? "");
}
export default function ManualConfigModal({
open,
onClose,
tool,
baseUrl,
apiKey,
model,
customCode,
}: ManualConfigModalProps) {
const [copied, setCopied] = useState(false);
if (!open) return null;
const source = customCode ?? tool.codeBlock;
const vars: Record<string, string> = { baseUrl, apiKey, model };
const rendered = source ? interpolate(source.code, vars) : "";
async function handleCopy() {
try {
await navigator.clipboard.writeText(rendered);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
// fallback: do nothing — clipboard unavailable in non-secure context
}
}
return (
/* Overlay */
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div
className="absolute inset-0 bg-black/30 backdrop-blur-sm"
onClick={onClose}
aria-hidden="true"
/>
{/* Dialog */}
<div
role="dialog"
aria-modal="true"
aria-label={`Configuração manual — ${tool.name}`}
className={cn(
"relative w-full bg-surface",
"border border-black/10 dark:border-white/10",
"rounded-xl shadow-2xl",
// Desktop: centered, max-w; Mobile: full-screen
"max-w-xl sm:max-w-2xl",
"max-h-screen sm:max-h-[90vh] overflow-y-auto"
)}
>
{/* Header */}
<div className="flex items-center justify-between p-5 border-b border-black/5 dark:border-white/5">
<div>
<h2 className="text-base font-semibold text-text-main">
{tool.name} Configuração Manual
</h2>
{source && (
<p className="text-xs text-text-muted mt-0.5">
Linguagem: <span className="font-mono">{source.language}</span>
</p>
)}
</div>
<button
onClick={onClose}
aria-label="Fechar"
className="p-1.5 rounded-lg text-text-muted hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
>
<span className="material-symbols-outlined text-[20px]" aria-hidden="true">
close
</span>
</button>
</div>
{/* Body */}
<div className="p-5 flex flex-col gap-4">
{source ? (
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-text-main">Código de configuração</span>
<button
onClick={handleCopy}
className={cn(
"inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg transition-all",
copied
? "bg-green-500/10 text-green-600 dark:text-green-400"
: "bg-black/5 dark:bg-white/5 text-text-muted hover:text-text-main hover:bg-black/10 dark:hover:bg-white/10"
)}
>
<span className="material-symbols-outlined text-[14px]" aria-hidden="true">
{copied ? "check" : "content_copy"}
</span>
{copied ? "Copied!" : "Copy"}
</button>
</div>
<pre className="px-4 py-3 bg-black/5 dark:bg-white/5 rounded-lg font-mono text-xs overflow-x-auto whitespace-pre-wrap break-all border border-black/5 dark:border-white/5 leading-relaxed">
{rendered}
</pre>
</div>
) : (
<p className="text-sm text-text-muted">
Nenhum bloco de configuração disponível para {tool.name}.
</p>
)}
{/* Variable summary */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2 pt-2 border-t border-black/5 dark:border-white/5">
{(
[
{ key: "Base URL", val: baseUrl },
{ key: "API Key", val: apiKey ? `${apiKey.slice(0, 8)}` : "(não definida)" },
{ key: "Model", val: model },
] as Array<{ key: string; val: string }>
).map(({ key, val }) => (
<div key={key} className="flex flex-col gap-0.5">
<span className="text-[10px] uppercase tracking-wider text-text-muted">{key}</span>
<span className="text-xs font-mono text-text-main truncate">{val}</span>
</div>
))}
</div>
</div>
</div>
</div>
);
}

View File

@@ -6,12 +6,3 @@ export type { CliConceptCardProps, CliConceptType } from "./CliConceptCard";
export { default as CliComparisonCard } from "./CliComparisonCard";
export type { CliComparisonCardProps } from "./CliComparisonCard";
export { default as BaseUrlSelect } from "./BaseUrlSelect";
export type { BaseUrlSelectProps } from "./BaseUrlSelect";
export { default as ApiKeySelect } from "./ApiKeySelect";
export type { ApiKeySelectProps, ApiKeyEntry } from "./ApiKeySelect";
export { default as ManualConfigModal } from "./ManualConfigModal";
export type { ManualConfigModalProps, ManualConfigModalCustomCode } from "./ManualConfigModal";

View File

@@ -1,160 +0,0 @@
// @vitest-environment jsdom
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { ApiKeyEntry, ApiKeySelectProps } from "@/shared/components/cli/ApiKeySelect";
// ── Import ────────────────────────────────────────────────────────────────────
const { default: ApiKeySelect } = await import("@/shared/components/cli/ApiKeySelect");
// ── Helpers ───────────────────────────────────────────────────────────────────
const containers: HTMLElement[] = [];
function renderSelect(props: ApiKeySelectProps): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
act(() => {
root.render(<ApiKeySelect {...props} />);
});
return container;
}
const SAMPLE_KEYS: ApiKeyEntry[] = [
{ id: "key1", name: "Production Key", prefix: "sk-prod" },
{ id: "key2", name: "Dev Key", prefix: "sk-dev" },
];
// ── Lifecycle ─────────────────────────────────────────────────────────────────
beforeEach(() => {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
true;
});
afterEach(() => {
while (containers.length > 0) {
containers.pop()?.remove();
}
document.body.innerHTML = "";
});
// ── Tests ─────────────────────────────────────────────────────────────────────
describe("ApiKeySelect", () => {
it("renders a select element", () => {
const container = renderSelect({
keys: SAMPLE_KEYS,
value: "key1",
onChange: vi.fn(),
});
const select = container.querySelector("select");
expect(select).not.toBeNull();
});
it("renders all provided keys as options", () => {
const container = renderSelect({
keys: SAMPLE_KEYS,
value: "key1",
onChange: vi.fn(),
});
expect(container.textContent).toContain("Production Key");
expect(container.textContent).toContain("Dev Key");
});
it("renders prefix in option text", () => {
const container = renderSelect({
keys: SAMPLE_KEYS,
value: "key1",
onChange: vi.fn(),
});
expect(container.textContent).toContain("sk-prod");
});
it("always includes 'Inserir manualmente' option", () => {
const container = renderSelect({
keys: SAMPLE_KEYS,
value: "key1",
onChange: vi.fn(),
});
expect(container.textContent).toContain("Inserir manualmente");
});
it("does NOT show manual input by default when a known key is selected", () => {
const container = renderSelect({
keys: SAMPLE_KEYS,
value: "key1",
onChange: vi.fn(),
});
const input = container.querySelector("input");
expect(input).toBeNull();
});
it("shows manual input when value is not in keys list", () => {
// Passing a value that's not in the keys array triggers manual mode
const container = renderSelect({
keys: SAMPLE_KEYS,
value: "sk-somemanualvalue",
onChange: vi.fn(),
});
const input = container.querySelector("input");
expect(input).not.toBeNull();
});
it("calls onChange when selecting a different key", () => {
const onChange = vi.fn();
const container = renderSelect({
keys: SAMPLE_KEYS,
value: "key1",
onChange,
});
const select = container.querySelector("select") as HTMLSelectElement;
act(() => {
select.value = "key2";
select.dispatchEvent(new Event("change", { bubbles: true }));
});
expect(onChange).toHaveBeenCalledWith("key2");
});
it("selecting '__manual__' value reveals input", () => {
const onChange = vi.fn();
const container = renderSelect({
keys: SAMPLE_KEYS,
value: "key1",
onChange,
});
const select = container.querySelector("select") as HTMLSelectElement;
act(() => {
select.value = "__manual__";
select.dispatchEvent(new Event("change", { bubbles: true }));
});
const input = container.querySelector("input");
expect(input).not.toBeNull();
});
it("renders label when provided", () => {
const container = renderSelect({
keys: SAMPLE_KEYS,
value: "key1",
onChange: vi.fn(),
label: "Auth Key",
});
expect(container.textContent).toContain("Auth Key");
});
it("disables select when disabled=true", () => {
const container = renderSelect({
keys: SAMPLE_KEYS,
value: "key1",
onChange: vi.fn(),
disabled: true,
});
const select = container.querySelector("select") as HTMLSelectElement;
expect(select.disabled).toBe(true);
});
});

View File

@@ -1,159 +0,0 @@
// @vitest-environment jsdom
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { BaseUrlSelectProps } from "@/shared/components/cli/BaseUrlSelect";
// ── Import ────────────────────────────────────────────────────────────────────
const { default: BaseUrlSelect } = await import("@/shared/components/cli/BaseUrlSelect");
// ── Helpers ───────────────────────────────────────────────────────────────────
const containers: HTMLElement[] = [];
function renderSelect(props: BaseUrlSelectProps): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
act(() => {
root.render(<BaseUrlSelect {...props} />);
});
return container;
}
// ── Lifecycle ─────────────────────────────────────────────────────────────────
beforeEach(() => {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
true;
// Stub window.location.origin
Object.defineProperty(window, "location", {
value: { origin: "http://localhost:20128" },
writable: true,
configurable: true,
});
});
afterEach(() => {
while (containers.length > 0) {
containers.pop()?.remove();
}
document.body.innerHTML = "";
});
// ── Tests ─────────────────────────────────────────────────────────────────────
describe("BaseUrlSelect", () => {
it("renders a select element", () => {
const container = renderSelect({
value: "http://localhost:20128",
onChange: vi.fn(),
cloudEnabled: false,
});
const select = container.querySelector("select");
expect(select).not.toBeNull();
});
it("shows Local option always", () => {
const container = renderSelect({
value: "http://localhost:20128",
onChange: vi.fn(),
cloudEnabled: false,
});
expect(container.textContent).toContain("Local");
});
it("shows Cloud option when cloudEnabled=true and cloudUrl is set", () => {
const container = renderSelect({
value: "http://localhost:20128",
onChange: vi.fn(),
cloudEnabled: true,
cloudUrl: "https://cloud.example.com",
});
expect(container.textContent).toContain("Cloud");
expect(container.textContent).toContain("cloud.example.com");
});
it("does NOT show Cloud option when cloudEnabled=false", () => {
const container = renderSelect({
value: "http://localhost:20128",
onChange: vi.fn(),
cloudEnabled: false,
cloudUrl: "https://cloud.example.com",
});
// "Cloud" option should not appear in select options
const select = container.querySelector("select");
const options = Array.from(select?.options ?? []);
const cloudOption = options.find((o) => o.value === "cloud");
expect(cloudOption).toBeUndefined();
});
it("shows Custom option always", () => {
const container = renderSelect({
value: "http://localhost:20128",
onChange: vi.fn(),
cloudEnabled: false,
});
const select = container.querySelector("select");
const options = Array.from(select?.options ?? []);
const customOption = options.find((o) => o.value === "custom");
expect(customOption).toBeDefined();
});
it("reveals custom input when value does not match local or cloud", () => {
const container = renderSelect({
value: "https://custom.example.com",
onChange: vi.fn(),
cloudEnabled: false,
});
// Since value doesn't match local, it should be in custom mode showing an input
const input = container.querySelector("input");
expect(input).not.toBeNull();
});
it("calls onChange when custom input changes", () => {
const onChange = vi.fn();
const container = renderSelect({
value: "https://custom.example.com",
onChange,
cloudEnabled: false,
});
const input = container.querySelector("input") as HTMLInputElement;
expect(input).not.toBeNull();
// Use React's synthetic event via nativeInputValueSetter
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value"
)?.set;
act(() => {
nativeInputValueSetter?.call(input, "https://new.example.com");
input.dispatchEvent(new Event("change", { bubbles: true }));
});
expect(onChange).toHaveBeenCalledWith("https://new.example.com");
});
it("renders label when provided", () => {
const container = renderSelect({
value: "http://localhost:20128",
onChange: vi.fn(),
cloudEnabled: false,
label: "Endpoint URL",
});
expect(container.textContent).toContain("Endpoint URL");
});
it("disables select when disabled=true", () => {
const container = renderSelect({
value: "http://localhost:20128",
onChange: vi.fn(),
cloudEnabled: false,
disabled: true,
});
const select = container.querySelector("select") as HTMLSelectElement;
expect(select.disabled).toBe(true);
});
});

View File

@@ -1,238 +0,0 @@
// @vitest-environment jsdom
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { CliCatalogEntry } from "@/shared/schemas/cliCatalog";
import type {
ManualConfigModalProps,
ManualConfigModalCustomCode,
} from "@/shared/components/cli/ManualConfigModal";
// ── Import ────────────────────────────────────────────────────────────────────
const { default: ManualConfigModal } = await import("@/shared/components/cli/ManualConfigModal");
// ── Helpers ───────────────────────────────────────────────────────────────────
const containers: HTMLElement[] = [];
function makeTool(overrides: Partial<CliCatalogEntry> = {}): CliCatalogEntry {
return {
id: "continue",
name: "Continue",
icon: "terminal",
color: "#7C3AED",
description: "Continue AI coding assistant",
docsUrl: "https://example.com",
configType: "guide",
category: "code",
vendor: "continue.dev",
acpSpawnable: false,
baseUrlSupport: "full",
codeBlock: {
language: "json",
code: '{\n "baseURL": "{{baseUrl}}",\n "apiKey": "{{apiKey}}",\n "model": "{{model}}"\n}',
},
...overrides,
};
}
function renderModal(props: ManualConfigModalProps): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
act(() => {
root.render(<ManualConfigModal {...props} />);
});
return container;
}
// ── Lifecycle ─────────────────────────────────────────────────────────────────
beforeEach(() => {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
true;
// Mock clipboard
const writeText = vi.fn(() => Promise.resolve());
Object.defineProperty(navigator, "clipboard", {
value: { writeText },
writable: true,
configurable: true,
});
});
afterEach(() => {
while (containers.length > 0) {
containers.pop()?.remove();
}
document.body.innerHTML = "";
});
// ── Tests ─────────────────────────────────────────────────────────────────────
describe("ManualConfigModal", () => {
it("does not render when open=false", () => {
const container = renderModal({
open: false,
onClose: vi.fn(),
tool: makeTool(),
baseUrl: "http://localhost:20128",
apiKey: "sk-test",
model: "gpt-4o",
});
const dialog = container.querySelector('[role="dialog"]');
expect(dialog).toBeNull();
});
it("renders when open=true", () => {
const container = renderModal({
open: true,
onClose: vi.fn(),
tool: makeTool(),
baseUrl: "http://localhost:20128",
apiKey: "sk-test",
model: "gpt-4o",
});
const dialog = container.querySelector('[role="dialog"]');
expect(dialog).not.toBeNull();
});
it("interpolates {{baseUrl}} in code block", () => {
const container = renderModal({
open: true,
onClose: vi.fn(),
tool: makeTool(),
baseUrl: "http://localhost:20128",
apiKey: "sk-test",
model: "gpt-4o",
});
const pre = container.querySelector("pre");
expect(pre?.textContent).toContain("http://localhost:20128");
expect(pre?.textContent).not.toContain("{{baseUrl}}");
});
it("interpolates {{apiKey}} in code block", () => {
const container = renderModal({
open: true,
onClose: vi.fn(),
tool: makeTool(),
baseUrl: "http://localhost:20128",
apiKey: "sk-test-key",
model: "gpt-4o",
});
const pre = container.querySelector("pre");
expect(pre?.textContent).toContain("sk-test-key");
expect(pre?.textContent).not.toContain("{{apiKey}}");
});
it("interpolates {{model}} in code block", () => {
const container = renderModal({
open: true,
onClose: vi.fn(),
tool: makeTool(),
baseUrl: "http://localhost:20128",
apiKey: "sk-test",
model: "claude-sonnet-4-5",
});
const pre = container.querySelector("pre");
expect(pre?.textContent).toContain("claude-sonnet-4-5");
expect(pre?.textContent).not.toContain("{{model}}");
});
it("uses customCode when provided instead of tool.codeBlock", () => {
const customCode: ManualConfigModalCustomCode = {
language: "bash",
code: "export BASE_URL={{baseUrl}}",
};
const tool = makeTool({ codeBlock: undefined });
const container = renderModal({
open: true,
onClose: vi.fn(),
tool,
baseUrl: "https://custom.example.com",
apiKey: "sk-test",
model: "gpt-4o",
customCode,
});
const pre = container.querySelector("pre");
expect(pre?.textContent).toContain("https://custom.example.com");
expect(pre?.textContent).toContain("export BASE_URL=");
});
it("calls navigator.clipboard.writeText when Copy button is clicked", async () => {
const container = renderModal({
open: true,
onClose: vi.fn(),
tool: makeTool(),
baseUrl: "http://localhost:20128",
apiKey: "sk-test",
model: "gpt-4o",
});
const copyButton = Array.from(container.querySelectorAll("button")).find((b) =>
b.textContent?.includes("Copy")
);
expect(copyButton).not.toBeUndefined();
await act(async () => {
copyButton!.click();
await Promise.resolve();
});
expect(navigator.clipboard.writeText).toHaveBeenCalled();
});
it("shows Copied! feedback after copying", async () => {
const container = renderModal({
open: true,
onClose: vi.fn(),
tool: makeTool(),
baseUrl: "http://localhost:20128",
apiKey: "sk-test",
model: "gpt-4o",
});
const copyButton = Array.from(container.querySelectorAll("button")).find((b) =>
b.textContent?.includes("Copy")
);
await act(async () => {
copyButton!.click();
await Promise.resolve();
});
expect(container.textContent).toContain("Copied!");
});
it("calls onClose when backdrop is clicked", () => {
const onClose = vi.fn();
const container = renderModal({
open: true,
onClose,
tool: makeTool(),
baseUrl: "http://localhost:20128",
apiKey: "sk-test",
model: "gpt-4o",
});
const overlay = container.querySelector('[aria-hidden="true"]') as HTMLElement;
act(() => {
overlay?.click();
});
expect(onClose).toHaveBeenCalled();
});
it("shows tool name in header", () => {
const container = renderModal({
open: true,
onClose: vi.fn(),
tool: makeTool({ name: "My CLI Tool" }),
baseUrl: "http://localhost:20128",
apiKey: "sk-test",
model: "gpt-4o",
});
expect(container.textContent).toContain("My CLI Tool");
});
});