test(coverage): bootstrap vitest unit infra and add tests at 90%+ per file (#1245) (#1269)

* test(coverage): bootstrap vitest unit infra and add tests at 90%+ per file (#1245)

Establishes the unit-test runtime AGENTS.md mandates: a vitest "unit"
project alongside the existing storybook project, RTL + jest-dom +
happy-dom, a Mantine-aware render helper, and a 90%-line per-file
coverage gate enforced by CI. Adds 66 test files covering all 62
components under elements/groups/screens/views, plus core/mcp and
clients/web/src/utils. Total: 577 tests, 98.7% line coverage globally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(coverage): address PR #1269 review feedback

- AGENTS.md: correct coverage gate description (lines ≥ 90, statements ≥ 85,
  functions ≥ 80, branches ≥ 50) with rationale for the relaxed branch
  threshold
- InspectorView.test.tsx: document what the Math.random() mock values
  (0.1 / 0.99) control vs. STUB_SUCCESS_RATE in InspectorView.tsx
- vite.config.ts / tsconfig.test.json: drop unused `globals: true` and the
  `vitest/globals` type now that every test file imports describe/it/expect/vi
  explicitly; manual `afterEach(cleanup)` in setup.ts remains required because
  testing-library auto-cleanup relies on globalThis.afterEach

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cliff Hall 2026-05-01 15:15:32 -04:00 • committed by GitHub
commit 75f74d64c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 8117 additions and 30 deletions

View file

@ -0,0 +1,82 @@
import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import type { ElicitRequestFormParams } from "@modelcontextprotocol/sdk/types.js";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { ElicitationFormPanel } from "./ElicitationFormPanel";
const dbRequest: ElicitRequestFormParams = {
message: "Please provide database connection details.",
requestedSchema: {
type: "object" as const,
properties: {
host: { type: "string" as const, title: "Host" },
port: { type: "string" as const, title: "Port" },
},
},
};
const baseProps = {
request: dbRequest,
serverName: "postgres-server",
values: {},
onChange: vi.fn(),
onSubmit: vi.fn(),
onCancel: vi.fn(),
};
describe("ElicitationFormPanel", () => {
it("renders the quoted request message", () => {
renderWithMantine(<ElicitationFormPanel {...baseProps} />);
expect(
screen.getByText(/Please provide database connection details\./),
).toBeInTheDocument();
});
it("renders the warning with the server name", () => {
renderWithMantine(<ElicitationFormPanel {...baseProps} />);
expect(screen.getByText("Warning")).toBeInTheDocument();
expect(screen.getByText(/postgres-server/)).toBeInTheDocument();
});
it("renders schema fields from the requested schema", () => {
renderWithMantine(<ElicitationFormPanel {...baseProps} />);
expect(screen.getByLabelText("Host")).toBeInTheDocument();
expect(screen.getByLabelText("Port")).toBeInTheDocument();
});
it("renders Submit and Cancel buttons", () => {
renderWithMantine(<ElicitationFormPanel {...baseProps} />);
expect(screen.getByRole("button", { name: "Submit" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument();
});
it("invokes onSubmit when Submit is clicked", async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
renderWithMantine(
<ElicitationFormPanel {...baseProps} onSubmit={onSubmit} />,
);
await user.click(screen.getByRole("button", { name: "Submit" }));
expect(onSubmit).toHaveBeenCalledTimes(1);
});
it("invokes onCancel when Cancel is clicked", async () => {
const user = userEvent.setup();
const onCancel = vi.fn();
renderWithMantine(
<ElicitationFormPanel {...baseProps} onCancel={onCancel} />,
);
await user.click(screen.getByRole("button", { name: "Cancel" }));
expect(onCancel).toHaveBeenCalledTimes(1);
});
it("invokes onChange when a schema field is updated", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
renderWithMantine(
<ElicitationFormPanel {...baseProps} onChange={onChange} />,
);
await user.type(screen.getByLabelText("Host"), "l");
expect(onChange).toHaveBeenCalledWith({ host: "l" });
});
});

View file

@ -0,0 +1,77 @@
import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { ElicitationUrlPanel } from "./ElicitationUrlPanel";
const baseProps = {
message: "Please authenticate with the external service.",
url: "https://auth.example.com/oauth/authorize?client_id=mcp-inspector",
requestId: "elicit-abc-123",
isWaiting: false,
onCopyUrl: vi.fn(),
onOpenInBrowser: vi.fn(),
onCancel: vi.fn(),
};
describe("ElicitationUrlPanel", () => {
it("renders the message, url, and request id", () => {
renderWithMantine(<ElicitationUrlPanel {...baseProps} />);
expect(
screen.getByText("Please authenticate with the external service."),
).toBeInTheDocument();
expect(screen.getByText(baseProps.url)).toBeInTheDocument();
expect(
screen.getByText(`Request ID: ${baseProps.requestId}`),
).toBeInTheDocument();
});
it("renders the warning alert", () => {
renderWithMantine(<ElicitationUrlPanel {...baseProps} />);
expect(screen.getByText("Warning")).toBeInTheDocument();
expect(
screen.getByText(/This will open an external URL/),
).toBeInTheDocument();
});
it("does not render waiting indicator when isWaiting is false", () => {
renderWithMantine(<ElicitationUrlPanel {...baseProps} />);
expect(
screen.queryByText("Waiting for completion..."),
).not.toBeInTheDocument();
});
it("renders waiting indicator when isWaiting is true", () => {
renderWithMantine(<ElicitationUrlPanel {...baseProps} isWaiting />);
expect(screen.getByText("Waiting for completion...")).toBeInTheDocument();
});
it("invokes onCopyUrl when Copy URL is clicked", async () => {
const user = userEvent.setup();
const onCopyUrl = vi.fn();
renderWithMantine(
<ElicitationUrlPanel {...baseProps} onCopyUrl={onCopyUrl} />,
);
await user.click(screen.getByRole("button", { name: "Copy URL" }));
expect(onCopyUrl).toHaveBeenCalledTimes(1);
});
it("invokes onOpenInBrowser when Open in Browser is clicked", async () => {
const user = userEvent.setup();
const onOpenInBrowser = vi.fn();
renderWithMantine(
<ElicitationUrlPanel {...baseProps} onOpenInBrowser={onOpenInBrowser} />,
);
await user.click(screen.getByRole("button", { name: "Open in Browser" }));
expect(onOpenInBrowser).toHaveBeenCalledTimes(1);
});
it("invokes onCancel when Cancel is clicked", async () => {
const user = userEvent.setup();
const onCancel = vi.fn();
renderWithMantine(
<ElicitationUrlPanel {...baseProps} onCancel={onCancel} />,
);
await user.click(screen.getByRole("button", { name: "Cancel" }));
expect(onCancel).toHaveBeenCalledTimes(1);
});
});

View file

@ -0,0 +1,328 @@
import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { ExperimentalFeaturesPanel } from "./ExperimentalFeaturesPanel";
import type { RequestHistoryItem } from "./ExperimentalFeaturesPanel";
const baseProps = {
serverExperimental: undefined,
clientExperimental: undefined,
requestDraft:
'{"jsonrpc":"2.0","id":1,"method":"experimental/x","params":{}}',
customHeaders: [],
requestHistory: [],
onToggleClientCapability: vi.fn(),
onRequestChange: vi.fn(),
onSendRequest: vi.fn(),
onAddHeader: vi.fn(),
onRemoveHeader: vi.fn(),
onHeaderChange: vi.fn(),
onCopyResponse: vi.fn(),
onTestCapability: vi.fn(),
};
describe("ExperimentalFeaturesPanel", () => {
it("renders the warning alert and section titles", () => {
renderWithMantine(<ExperimentalFeaturesPanel {...baseProps} />);
expect(
screen.getByText(
"These features are non-standard and may change or be removed.",
),
).toBeInTheDocument();
expect(
screen.getByText("Server Experimental Capabilities:"),
).toBeInTheDocument();
expect(
screen.getByText("Client Experimental Capabilities:"),
).toBeInTheDocument();
expect(screen.getByText("Advanced JSON-RPC Tester")).toBeInTheDocument();
});
it("shows the empty state when no server experimental capabilities are present", () => {
renderWithMantine(<ExperimentalFeaturesPanel {...baseProps} />);
expect(
screen.getByText("No experimental capabilities"),
).toBeInTheDocument();
});
it("renders server capability cards with description and methods", () => {
renderWithMantine(
<ExperimentalFeaturesPanel
{...baseProps}
serverExperimental={{
"experimental/streaming": {
description: "Supports streaming responses",
methods: [
"experimental/stream.start",
"experimental/stream.cancel",
],
},
}}
/>,
);
expect(screen.getByText("experimental/streaming")).toBeInTheDocument();
expect(
screen.getByText("Supports streaming responses"),
).toBeInTheDocument();
expect(
screen.getByText(
"Methods: experimental/stream.start, experimental/stream.cancel",
),
).toBeInTheDocument();
});
it("renders the capability card without description or methods when not provided", () => {
renderWithMantine(
<ExperimentalFeaturesPanel
{...baseProps}
serverExperimental={{
"experimental/bare": {},
}}
/>,
);
expect(screen.getByText("experimental/bare")).toBeInTheDocument();
expect(screen.queryByText(/^Methods:/)).not.toBeInTheDocument();
});
it("invokes onTestCapability when Test is clicked on a capability card", async () => {
const user = userEvent.setup();
const onTestCapability = vi.fn();
renderWithMantine(
<ExperimentalFeaturesPanel
{...baseProps}
onTestCapability={onTestCapability}
serverExperimental={{
"experimental/echo": { description: "Echoes" },
}}
/>,
);
await user.click(screen.getByRole("button", { name: /Test/ }));
expect(onTestCapability).toHaveBeenCalledWith("experimental/echo");
});
it("renders the known client capability toggles with their friendly labels", () => {
renderWithMantine(<ExperimentalFeaturesPanel {...baseProps} />);
expect(
screen.getByRole("checkbox", { name: "Custom sampling" }),
).toBeInTheDocument();
expect(
screen.getByRole("checkbox", { name: "Batch requests" }),
).toBeInTheDocument();
});
it("marks a known client capability checkbox as checked when enabled", () => {
renderWithMantine(
<ExperimentalFeaturesPanel
{...baseProps}
clientExperimental={{ "experimental/batchRequests": {} }}
/>,
);
const sampling = screen.getByRole("checkbox", {
name: "Custom sampling",
}) as HTMLInputElement;
const batch = screen.getByRole("checkbox", {
name: "Batch requests",
}) as HTMLInputElement;
expect(sampling.checked).toBe(false);
expect(batch.checked).toBe(true);
});
it("renders unknown client capabilities by their key", () => {
renderWithMantine(
<ExperimentalFeaturesPanel
{...baseProps}
clientExperimental={{
"experimental/customSampling": {},
"experimental/futureFeatureXyz": {},
}}
/>,
);
expect(
screen.getByRole("checkbox", { name: "experimental/futureFeatureXyz" }),
).toBeInTheDocument();
});
it("invokes onToggleClientCapability when a client checkbox is clicked", async () => {
const user = userEvent.setup();
const onToggleClientCapability = vi.fn();
renderWithMantine(
<ExperimentalFeaturesPanel
{...baseProps}
onToggleClientCapability={onToggleClientCapability}
/>,
);
await user.click(screen.getByRole("checkbox", { name: "Custom sampling" }));
expect(onToggleClientCapability).toHaveBeenCalledWith(
"experimental/customSampling",
true,
);
});
it("invokes onAddHeader when + Add Header is clicked", async () => {
const user = userEvent.setup();
const onAddHeader = vi.fn();
renderWithMantine(
<ExperimentalFeaturesPanel {...baseProps} onAddHeader={onAddHeader} />,
);
await user.click(screen.getByRole("button", { name: "+ Add Header" }));
expect(onAddHeader).toHaveBeenCalledTimes(1);
});
it("renders custom header rows and invokes onHeaderChange when key changes", async () => {
const user = userEvent.setup();
const onHeaderChange = vi.fn();
renderWithMantine(
<ExperimentalFeaturesPanel
{...baseProps}
customHeaders={[{ key: "X-Auth", value: "Bearer 1" }]}
onHeaderChange={onHeaderChange}
/>,
);
const keyInput = screen.getByDisplayValue("X-Auth");
await user.type(keyInput, "Z");
expect(onHeaderChange).toHaveBeenCalled();
const lastCall =
onHeaderChange.mock.calls[onHeaderChange.mock.calls.length - 1];
expect(lastCall[0]).toBe(0);
expect(lastCall[2]).toBe("Bearer 1");
});
it("invokes onHeaderChange when value changes for a header", async () => {
const user = userEvent.setup();
const onHeaderChange = vi.fn();
renderWithMantine(
<ExperimentalFeaturesPanel
{...baseProps}
customHeaders={[{ key: "X-Auth", value: "abc" }]}
onHeaderChange={onHeaderChange}
/>,
);
const valueInput = screen.getByDisplayValue("abc");
await user.type(valueInput, "Z");
expect(onHeaderChange).toHaveBeenCalled();
const lastCall =
onHeaderChange.mock.calls[onHeaderChange.mock.calls.length - 1];
expect(lastCall[0]).toBe(0);
expect(lastCall[1]).toBe("X-Auth");
});
it("invokes onRemoveHeader with the header index when the remove icon is clicked", async () => {
const user = userEvent.setup();
const onRemoveHeader = vi.fn();
renderWithMantine(
<ExperimentalFeaturesPanel
{...baseProps}
customHeaders={[
{ key: "X-Auth", value: "abc" },
{ key: "X-Other", value: "def" },
]}
onRemoveHeader={onRemoveHeader}
/>,
);
const removeButtons = screen.getAllByRole("button", { name: "✕" });
await user.click(removeButtons[1]);
expect(onRemoveHeader).toHaveBeenCalledWith(1);
});
it("invokes onRequestChange when the request textarea changes", async () => {
const user = userEvent.setup();
const onRequestChange = vi.fn();
renderWithMantine(
<ExperimentalFeaturesPanel
{...baseProps}
requestDraft=""
onRequestChange={onRequestChange}
/>,
);
const textarea = screen.getByLabelText("Request");
await user.type(textarea, "x");
expect(onRequestChange).toHaveBeenCalledWith("x");
});
it("invokes onSendRequest when Send Request is clicked", async () => {
const user = userEvent.setup();
const onSendRequest = vi.fn();
renderWithMantine(
<ExperimentalFeaturesPanel
{...baseProps}
onSendRequest={onSendRequest}
/>,
);
await user.click(screen.getByRole("button", { name: "Send Request" }));
expect(onSendRequest).toHaveBeenCalledTimes(1);
});
it("renders a successful response without an Error badge", () => {
renderWithMantine(
<ExperimentalFeaturesPanel
{...baseProps}
response={{
jsonrpc: "2.0",
id: 1,
result: { ok: true },
}}
/>,
);
expect(screen.getByText("Response")).toBeInTheDocument();
expect(screen.queryByText("Error")).not.toBeInTheDocument();
});
it("renders an error response with the Error badge", () => {
renderWithMantine(
<ExperimentalFeaturesPanel
{...baseProps}
response={{
jsonrpc: "2.0",
id: 1,
error: { code: -32601, message: "Method not found" },
}}
/>,
);
expect(screen.getByText("Response")).toBeInTheDocument();
expect(screen.getByText("Error")).toBeInTheDocument();
});
it("invokes onCopyResponse when Copy is clicked", async () => {
const user = userEvent.setup();
const onCopyResponse = vi.fn();
renderWithMantine(
<ExperimentalFeaturesPanel
{...baseProps}
response={{ jsonrpc: "2.0", id: 1, result: { ok: true } }}
onCopyResponse={onCopyResponse}
/>,
);
await user.click(screen.getByRole("button", { name: "Copy" }));
expect(onCopyResponse).toHaveBeenCalledTimes(1);
});
it("renders the request history table when entries are present", () => {
const items: RequestHistoryItem[] = [
{
timestamp: new Date("2026-03-17T10:30:15Z"),
method: "experimental/metrics.get",
status: "success",
durationMs: 42,
},
{
timestamp: new Date("2026-03-17T10:29:50Z"),
method: "experimental/echo",
status: "error",
durationMs: 120,
},
];
renderWithMantine(
<ExperimentalFeaturesPanel {...baseProps} requestHistory={items} />,
);
expect(screen.getByText("Request History:")).toBeInTheDocument();
expect(screen.getByText("experimental/metrics.get")).toBeInTheDocument();
expect(screen.getByText("experimental/echo")).toBeInTheDocument();
expect(screen.getByText("42ms")).toBeInTheDocument();
expect(screen.getByText("120ms")).toBeInTheDocument();
});
it("does not render the history section when requestHistory is empty", () => {
renderWithMantine(<ExperimentalFeaturesPanel {...baseProps} />);
expect(screen.queryByText("Request History:")).not.toBeInTheDocument();
});
});

View file

@ -0,0 +1,61 @@
import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import type { MessageMethod } from "@inspector/core/mcp/types.js";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { HistoryControls } from "./HistoryControls";
const baseProps = {
searchText: "",
availableMethods: ["tools/list", "prompts/list"] as MessageMethod[],
onSearchChange: vi.fn(),
onMethodFilterChange: vi.fn(),
};
describe("HistoryControls", () => {
it("renders the title and search input", () => {
renderWithMantine(<HistoryControls {...baseProps} />);
expect(screen.getByText("History")).toBeInTheDocument();
expect(screen.getByPlaceholderText("Search...")).toBeInTheDocument();
});
it("invokes onSearchChange when typing in the search input", async () => {
const user = userEvent.setup();
const onSearchChange = vi.fn();
renderWithMantine(
<HistoryControls {...baseProps} onSearchChange={onSearchChange} />,
);
await user.type(screen.getByPlaceholderText("Search..."), "a");
expect(onSearchChange).toHaveBeenCalledWith("a");
});
it("renders the method filter placeholder", () => {
renderWithMantine(<HistoryControls {...baseProps} />);
expect(screen.getByPlaceholderText("All methods")).toBeInTheDocument();
});
it("displays the active method filter value", () => {
renderWithMantine(
<HistoryControls {...baseProps} methodFilter="tools/list" />,
);
const inputs = screen.getAllByDisplayValue("tools/list");
expect(inputs.length).toBeGreaterThan(0);
});
it("invokes onMethodFilterChange with undefined when cleared", async () => {
const user = userEvent.setup();
const onMethodFilterChange = vi.fn();
const { container } = renderWithMantine(
<HistoryControls
{...baseProps}
methodFilter="tools/list"
onMethodFilterChange={onMethodFilterChange}
/>,
);
const clearButton = container.querySelector(
"button.mantine-InputClearButton-root",
) as HTMLButtonElement | null;
expect(clearButton).not.toBeNull();
await user.click(clearButton!);
expect(onMethodFilterChange).toHaveBeenCalledWith(undefined);
});
});

View file

@ -0,0 +1,233 @@
import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import type { MessageEntry } from "@inspector/core/mcp/types.js";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { HistoryEntry } from "./HistoryEntry";
const successEntry: MessageEntry = {
id: "req-1",
timestamp: new Date("2026-03-17T10:30:00Z"),
direction: "request",
message: {
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: { name: "get_weather", arguments: { city: "San Francisco" } },
},
response: {
jsonrpc: "2.0",
id: 1,
result: {
content: [{ type: "text", text: "18C" }],
},
},
duration: 142,
};
const errorEntry: MessageEntry = {
id: "req-2",
timestamp: new Date("2026-03-17T10:31:15Z"),
direction: "request",
message: {
jsonrpc: "2.0",
id: 2,
method: "tools/call",
params: { name: "query_database" },
},
response: {
jsonrpc: "2.0",
id: 2,
error: { code: -32000, message: "Connection timeout" },
},
duration: 3200,
};
const resourceReadEntry: MessageEntry = {
id: "req-3",
timestamp: new Date("2026-03-17T10:33:00Z"),
direction: "request",
message: {
jsonrpc: "2.0",
id: 3,
method: "resources/read",
params: { uri: "file:///config.json" },
},
response: {
jsonrpc: "2.0",
id: 3,
result: {
contents: [{ uri: "file:///config.json", text: '{"debug": true}' }],
},
},
duration: 45,
};
const pendingEntry: MessageEntry = {
id: "req-4",
timestamp: new Date("2026-03-17T10:34:00Z"),
direction: "request",
message: {
jsonrpc: "2.0",
id: 4,
method: "tools/call",
params: { name: "long_operation" },
},
};
const noParamsEntry: MessageEntry = {
id: "req-5",
timestamp: new Date("2026-03-17T10:35:00Z"),
direction: "request",
message: {
jsonrpc: "2.0",
id: 5,
method: "tools/list",
},
response: {
jsonrpc: "2.0",
id: 5,
result: { tools: [] },
},
};
const baseProps = {
isPinned: false,
isListExpanded: false,
onReplay: vi.fn(),
onTogglePin: vi.fn(),
};
describe("HistoryEntry", () => {
it("renders the method, target name, status OK, and duration", () => {
renderWithMantine(<HistoryEntry {...baseProps} entry={successEntry} />);
expect(screen.getByText("tools/call")).toBeInTheDocument();
expect(screen.getByText("get_weather")).toBeInTheDocument();
expect(screen.getByText("OK")).toBeInTheDocument();
expect(screen.getByText("142ms")).toBeInTheDocument();
});
it("renders the URI target for resources/read", () => {
renderWithMantine(
<HistoryEntry {...baseProps} entry={resourceReadEntry} />,
);
expect(screen.getByText("file:///config.json")).toBeInTheDocument();
expect(screen.getByText("resources/read")).toBeInTheDocument();
});
it("renders Error status when response has error", () => {
renderWithMantine(<HistoryEntry {...baseProps} entry={errorEntry} />);
expect(screen.getByText("Error")).toBeInTheDocument();
});
it("renders Pending status when no response present", () => {
renderWithMantine(<HistoryEntry {...baseProps} entry={pendingEntry} />);
expect(screen.getByText("Pending")).toBeInTheDocument();
});
it("renders Pin label when not pinned", () => {
renderWithMantine(<HistoryEntry {...baseProps} entry={successEntry} />);
expect(screen.getByRole("button", { name: "Pin" })).toBeInTheDocument();
});
it("renders Unpin label when pinned", () => {
renderWithMantine(
<HistoryEntry {...baseProps} entry={successEntry} isPinned={true} />,
);
expect(screen.getByRole("button", { name: "Unpin" })).toBeInTheDocument();
});
it("invokes onReplay when Replay button is clicked", async () => {
const user = userEvent.setup();
const onReplay = vi.fn();
renderWithMantine(
<HistoryEntry {...baseProps} entry={successEntry} onReplay={onReplay} />,
);
await user.click(screen.getByRole("button", { name: "Replay" }));
expect(onReplay).toHaveBeenCalledTimes(1);
});
it("invokes onTogglePin when Pin button is clicked", async () => {
const user = userEvent.setup();
const onTogglePin = vi.fn();
renderWithMantine(
<HistoryEntry
{...baseProps}
entry={successEntry}
onTogglePin={onTogglePin}
/>,
);
await user.click(screen.getByRole("button", { name: "Pin" }));
expect(onTogglePin).toHaveBeenCalledTimes(1);
});
it("toggles the local expand/collapse state when clicking Expand/Collapse", async () => {
const user = userEvent.setup();
renderWithMantine(<HistoryEntry {...baseProps} entry={successEntry} />);
expect(screen.getByRole("button", { name: "Expand" })).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Expand" }));
expect(
screen.getByRole("button", { name: "Collapse" }),
).toBeInTheDocument();
});
it("starts expanded when isListExpanded is true and shows Parameters and Response", () => {
renderWithMantine(
<HistoryEntry
{...baseProps}
entry={successEntry}
isListExpanded={true}
/>,
);
expect(screen.getByText("Parameters:")).toBeInTheDocument();
expect(screen.getByText("Response:")).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Collapse" }),
).toBeInTheDocument();
});
it("syncs the local expanded state when isListExpanded prop changes", () => {
const { rerender } = renderWithMantine(
<HistoryEntry {...baseProps} entry={successEntry} />,
);
expect(screen.getByRole("button", { name: "Expand" })).toBeInTheDocument();
rerender(
<HistoryEntry
{...baseProps}
entry={successEntry}
isListExpanded={true}
/>,
);
expect(
screen.getByRole("button", { name: "Collapse" }),
).toBeInTheDocument();
});
it("does not render the Parameters section when message has no params", () => {
renderWithMantine(
<HistoryEntry
{...baseProps}
entry={noParamsEntry}
isListExpanded={true}
/>,
);
expect(screen.queryByText("Parameters:")).not.toBeInTheDocument();
expect(screen.getByText("Response:")).toBeInTheDocument();
});
it("does not render the Response section when no response is present", () => {
renderWithMantine(
<HistoryEntry
{...baseProps}
entry={pendingEntry}
isListExpanded={true}
/>,
);
expect(screen.getByText("Parameters:")).toBeInTheDocument();
expect(screen.queryByText("Response:")).not.toBeInTheDocument();
});
it("does not render duration when duration is undefined", () => {
renderWithMantine(<HistoryEntry {...baseProps} entry={pendingEntry} />);
expect(screen.queryByText(/ms$/)).not.toBeInTheDocument();
});
});

View file

@ -0,0 +1,248 @@
import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import type { MessageEntry } from "@inspector/core/mcp/types.js";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { HistoryListPanel } from "./HistoryListPanel";
const sampleEntries: MessageEntry[] = [
{
id: "req-1",
timestamp: new Date("2026-03-17T10:00:00Z"),
direction: "request",
message: {
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: { name: "send_message", arguments: { message: "Hello!" } },
},
response: {
jsonrpc: "2.0",
id: 1,
result: { content: [{ type: "text", text: "Sent" }] },
},
duration: 120,
},
{
id: "req-2",
timestamp: new Date("2026-03-17T10:01:00Z"),
direction: "request",
message: {
jsonrpc: "2.0",
id: 2,
method: "resources/read",
params: { uri: "file:///config.json" },
},
response: {
jsonrpc: "2.0",
id: 2,
result: {
contents: [{ uri: "file:///config.json", text: "{}" }],
},
},
duration: 45,
},
{
id: "req-3",
timestamp: new Date("2026-03-17T10:02:00Z"),
direction: "request",
message: {
jsonrpc: "2.0",
id: 3,
method: "tools/list",
},
response: {
jsonrpc: "2.0",
id: 3,
result: { tools: [] },
},
duration: 80,
},
];
const baseProps = {
pinnedIds: new Set<string>(),
searchText: "",
onClearAll: vi.fn(),
onExport: vi.fn(),
onReplay: vi.fn(),
onTogglePin: vi.fn(),
};
describe("HistoryListPanel", () => {
it("renders the title and Export JSON button", () => {
renderWithMantine(<HistoryListPanel {...baseProps} entries={[]} />);
expect(screen.getByText("Requests")).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Export JSON" }),
).toBeInTheDocument();
});
it("renders the empty state when there are no entries", () => {
renderWithMantine(<HistoryListPanel {...baseProps} entries={[]} />);
expect(screen.getByText("No request history")).toBeInTheDocument();
});
it("renders the empty state when entries exist but none match the filter", () => {
renderWithMantine(
<HistoryListPanel
{...baseProps}
entries={sampleEntries}
searchText="zzznotfound"
/>,
);
expect(screen.getByText("No request history")).toBeInTheDocument();
});
it("renders the History title with count for unpinned entries", () => {
renderWithMantine(
<HistoryListPanel {...baseProps} entries={sampleEntries} />,
);
expect(screen.getByText("History (3)")).toBeInTheDocument();
});
it("renders the Pinned title with count when entries are pinned", () => {
renderWithMantine(
<HistoryListPanel
{...baseProps}
entries={sampleEntries}
pinnedIds={new Set(["req-1"])}
/>,
);
expect(screen.getByText("Pinned Requests (1)")).toBeInTheDocument();
expect(screen.getByText("History (2)")).toBeInTheDocument();
});
it("filters entries by searchText (case-insensitive)", () => {
renderWithMantine(
<HistoryListPanel
{...baseProps}
entries={sampleEntries}
searchText="config.json"
/>,
);
expect(screen.getByText("History (1)")).toBeInTheDocument();
});
it("filters entries by methodFilter", () => {
renderWithMantine(
<HistoryListPanel
{...baseProps}
entries={sampleEntries}
methodFilter="tools/list"
/>,
);
expect(screen.getByText("History (1)")).toBeInTheDocument();
});
it("invokes onExport when Export JSON is clicked", async () => {
const user = userEvent.setup();
const onExport = vi.fn();
renderWithMantine(
<HistoryListPanel
{...baseProps}
entries={sampleEntries}
onExport={onExport}
/>,
);
await user.click(screen.getByRole("button", { name: "Export JSON" }));
expect(onExport).toHaveBeenCalledTimes(1);
});
it("invokes onClearAll when Clear is clicked", async () => {
const user = userEvent.setup();
const onClearAll = vi.fn();
renderWithMantine(
<HistoryListPanel
{...baseProps}
entries={sampleEntries}
onClearAll={onClearAll}
/>,
);
await user.click(screen.getByRole("button", { name: "Clear" }));
expect(onClearAll).toHaveBeenCalledTimes(1);
});
it("invokes onReplay with the entry id when Replay is clicked", async () => {
const user = userEvent.setup();
const onReplay = vi.fn();
renderWithMantine(
<HistoryListPanel
{...baseProps}
entries={[sampleEntries[0]]}
onReplay={onReplay}
/>,
);
await user.click(screen.getByRole("button", { name: "Replay" }));
expect(onReplay).toHaveBeenCalledWith("req-1");
});
it("invokes onTogglePin with the entry id when Pin is clicked", async () => {
const user = userEvent.setup();
const onTogglePin = vi.fn();
renderWithMantine(
<HistoryListPanel
{...baseProps}
entries={[sampleEntries[0]]}
onTogglePin={onTogglePin}
/>,
);
await user.click(screen.getByRole("button", { name: "Pin" }));
expect(onTogglePin).toHaveBeenCalledWith("req-1");
});
it("renders only pinned section when all entries are pinned", () => {
renderWithMantine(
<HistoryListPanel
{...baseProps}
entries={[sampleEntries[0]]}
pinnedIds={new Set(["req-1"])}
/>,
);
expect(screen.getByText("Pinned Requests (1)")).toBeInTheDocument();
expect(screen.queryByText(/^History \(/)).not.toBeInTheDocument();
});
it("invokes onReplay and onTogglePin from the pinned section", async () => {
const user = userEvent.setup();
const onReplay = vi.fn();
const onTogglePin = vi.fn();
renderWithMantine(
<HistoryListPanel
{...baseProps}
entries={[sampleEntries[0]]}
pinnedIds={new Set(["req-1"])}
onReplay={onReplay}
onTogglePin={onTogglePin}
/>,
);
await user.click(screen.getByRole("button", { name: "Replay" }));
expect(onReplay).toHaveBeenCalledWith("req-1");
await user.click(screen.getByRole("button", { name: "Unpin" }));
expect(onTogglePin).toHaveBeenCalledWith("req-1");
});
it("toggles compact list state when ListToggle is clicked", async () => {
const user = userEvent.setup();
renderWithMantine(
<HistoryListPanel {...baseProps} entries={sampleEntries} />,
);
// Initially expanded — Collapse buttons exist on each entry
expect(
screen.getAllByRole("button", { name: "Collapse" }).length,
).toBeGreaterThan(0);
// Find the ListToggle button (last subtle toolbar button at top — has no accessible name)
const buttons = screen.getAllByRole("button");
const toggle = buttons.find(
(b) =>
b.textContent === "" && b.classList.contains("mantine-Button-root"),
);
expect(toggle).toBeDefined();
await user.click(toggle!);
// After toggle, entries collapsed — they show Expand
expect(
screen.getAllByRole("button", { name: "Expand" }).length,
).toBeGreaterThan(0);
});
});

View file

@ -0,0 +1,210 @@
import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import type { InspectorServerJsonDraft } from "@inspector/core/mcp/types.js";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import {
ImportServerJsonPanel,
type EnvVarInfo,
type PackageInfo,
type ValidationResult,
} from "./ImportServerJsonPanel";
const emptyDraft: InspectorServerJsonDraft = {
rawText: "",
envOverrides: {},
};
const baseHandlers = {
onJsonChange: vi.fn(),
onValidate: vi.fn(),
onSelectPackage: vi.fn(),
onEnvVarChange: vi.fn(),
onServerNameChange: vi.fn(),
onAddServer: vi.fn(),
onCancel: vi.fn(),
};
describe("ImportServerJsonPanel", () => {
it("renders the title and the action buttons", () => {
renderWithMantine(
<ImportServerJsonPanel
{...baseHandlers}
draft={emptyDraft}
validation={[]}
envVars={[]}
/>,
);
expect(
screen.getByText("Import MCP Registry server.json"),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Validate Again" }),
).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Add Server" }),
).toBeInTheDocument();
});
it("invokes onJsonChange when typing in the textarea", async () => {
const user = userEvent.setup();
const onJsonChange = vi.fn();
renderWithMantine(
<ImportServerJsonPanel
{...baseHandlers}
onJsonChange={onJsonChange}
draft={emptyDraft}
validation={[]}
envVars={[]}
/>,
);
// The first textbox is the JSON textarea
const allTextboxes = screen.getAllByRole("textbox");
// userEvent.type treats `{` as a key-descriptor delimiter; escape with `{{`
await user.type(allTextboxes[0], "x");
expect(onJsonChange).toHaveBeenCalledWith("x");
});
it("invokes onValidate, onCancel, and onAddServer when their buttons are clicked", async () => {
const user = userEvent.setup();
const onValidate = vi.fn();
const onCancel = vi.fn();
const onAddServer = vi.fn();
renderWithMantine(
<ImportServerJsonPanel
{...baseHandlers}
onValidate={onValidate}
onCancel={onCancel}
onAddServer={onAddServer}
draft={emptyDraft}
validation={[]}
envVars={[]}
/>,
);
await user.click(screen.getByRole("button", { name: "Validate Again" }));
await user.click(screen.getByRole("button", { name: "Cancel" }));
await user.click(screen.getByRole("button", { name: "Add Server" }));
expect(onValidate).toHaveBeenCalledTimes(1);
expect(onCancel).toHaveBeenCalledTimes(1);
expect(onAddServer).toHaveBeenCalledTimes(1);
});
it("renders all validation result types with messages", () => {
const validation: ValidationResult[] = [
{ type: "success", message: "Valid format" },
{ type: "warning", message: "Missing optional field" },
{ type: "info", message: "1 package found" },
{ type: "error", message: "Invalid JSON" },
];
renderWithMantine(
<ImportServerJsonPanel
{...baseHandlers}
draft={emptyDraft}
validation={validation}
envVars={[]}
/>,
);
expect(screen.getByText("Valid format")).toBeInTheDocument();
expect(screen.getByText("Missing optional field")).toBeInTheDocument();
expect(screen.getByText("1 package found")).toBeInTheDocument();
expect(screen.getByText("Invalid JSON")).toBeInTheDocument();
});
it("does not render the package selection block when fewer than 2 packages exist", () => {
const packages: PackageInfo[] = [
{ registryType: "npm", identifier: "x", runtimeHint: "node" },
];
renderWithMantine(
<ImportServerJsonPanel
{...baseHandlers}
draft={emptyDraft}
validation={[]}
envVars={[]}
packages={packages}
/>,
);
expect(screen.queryByText("Package Selection:")).not.toBeInTheDocument();
});
it("renders package selection radios and invokes onSelectPackage", async () => {
const user = userEvent.setup();
const onSelectPackage = vi.fn();
const packages: PackageInfo[] = [
{ registryType: "npm", identifier: "@scope/server", runtimeHint: "node" },
{ registryType: "pip", identifier: "server-py", runtimeHint: "python3" },
];
renderWithMantine(
<ImportServerJsonPanel
{...baseHandlers}
onSelectPackage={onSelectPackage}
draft={emptyDraft}
validation={[]}
envVars={[]}
packages={packages}
/>,
);
expect(screen.getByText("Package Selection:")).toBeInTheDocument();
expect(
screen.getByLabelText("npm: @scope/server (node)"),
).toBeInTheDocument();
await user.click(screen.getByLabelText("pip: server-py (python3)"));
expect(onSelectPackage).toHaveBeenCalledWith(1);
});
it("renders environment variable inputs and invokes onEnvVarChange", async () => {
const user = userEvent.setup();
const onEnvVarChange = vi.fn();
const envVars: EnvVarInfo[] = [
{
name: "API_KEY",
description: "Authentication key",
required: true,
value: "",
},
{ name: "DEBUG", required: false, value: "true" },
];
renderWithMantine(
<ImportServerJsonPanel
{...baseHandlers}
onEnvVarChange={onEnvVarChange}
draft={emptyDraft}
validation={[]}
envVars={envVars}
/>,
);
expect(screen.getByText("Environment Variables:")).toBeInTheDocument();
expect(screen.getByText("Authentication key")).toBeInTheDocument();
const apiKeyInput = screen.getByLabelText(/API_KEY/);
await user.type(apiKeyInput, "k");
expect(onEnvVarChange).toHaveBeenCalledWith("API_KEY", "k");
});
it("invokes onServerNameChange when typing in the override field", async () => {
const user = userEvent.setup();
const onServerNameChange = vi.fn();
renderWithMantine(
<ImportServerJsonPanel
{...baseHandlers}
onServerNameChange={onServerNameChange}
draft={emptyDraft}
validation={[]}
envVars={[]}
/>,
);
const nameInput = screen.getByLabelText(/Server Name/);
await user.type(nameInput, "X");
expect(onServerNameChange).toHaveBeenCalledWith("X");
});
it("renders the existing nameOverride value", () => {
renderWithMantine(
<ImportServerJsonPanel
{...baseHandlers}
draft={{ ...emptyDraft, nameOverride: "Custom Name" }}
validation={[]}
envVars={[]}
/>,
);
expect(screen.getByDisplayValue("Custom Name")).toBeInTheDocument();
});
});

View file

@ -0,0 +1,149 @@
import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import type { ElicitRequest } from "@modelcontextprotocol/sdk/types.js";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { InlineElicitationRequest } from "./InlineElicitationRequest";
const formRequest = {
message: "Please provide your database connection details.",
requestedSchema: {
type: "object" as const,
properties: {
host: { type: "string" as const, title: "Host" },
port: { type: "string" as const, title: "Port" },
},
},
} satisfies ElicitRequest["params"];
const urlRequest: ElicitRequest["params"] = {
mode: "url",
message: "Please authenticate via the external URL.",
url: "https://example.com/auth/callback?session=abc123",
elicitationId: "elicit-abc-123",
};
const baseProps = {
queuePosition: "1 of 1",
onChange: vi.fn(),
onSubmit: vi.fn(),
onCancel: vi.fn(),
};
describe("InlineElicitationRequest", () => {
it("renders the form badge, message, and queue position in form mode", () => {
renderWithMantine(
<InlineElicitationRequest
{...baseProps}
request={formRequest}
values={{}}
/>,
);
expect(screen.getByText("elicitation/create (form)")).toBeInTheDocument();
expect(screen.getByText(formRequest.message)).toBeInTheDocument();
expect(screen.getByText("1 of 1")).toBeInTheDocument();
});
it("renders schema fields and a Submit button in form mode", () => {
renderWithMantine(
<InlineElicitationRequest
{...baseProps}
request={formRequest}
values={{}}
/>,
);
expect(screen.getByText("Host")).toBeInTheDocument();
expect(screen.getByText("Port")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Submit" })).toBeInTheDocument();
});
it("invokes onSubmit when Submit is clicked", async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
renderWithMantine(
<InlineElicitationRequest
{...baseProps}
request={formRequest}
values={{}}
onSubmit={onSubmit}
/>,
);
await user.click(screen.getByRole("button", { name: "Submit" }));
expect(onSubmit).toHaveBeenCalledTimes(1);
});
it("invokes onCancel when Cancel is clicked", async () => {
const user = userEvent.setup();
const onCancel = vi.fn();
renderWithMantine(
<InlineElicitationRequest
{...baseProps}
request={formRequest}
values={{}}
onCancel={onCancel}
/>,
);
await user.click(screen.getByRole("button", { name: "Cancel" }));
expect(onCancel).toHaveBeenCalledTimes(1);
});
it("invokes onChange when a schema field changes", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
renderWithMantine(
<InlineElicitationRequest
{...baseProps}
request={formRequest}
values={{}}
onChange={onChange}
/>,
);
await user.type(screen.getByLabelText("Host"), "x");
expect(onChange).toHaveBeenCalled();
});
it("renders the URL badge and url in url mode", () => {
renderWithMantine(
<InlineElicitationRequest {...baseProps} request={urlRequest} />,
);
expect(screen.getByText("elicitation/create (url)")).toBeInTheDocument();
expect(screen.getByText(urlRequest.url)).toBeInTheDocument();
});
it("does not render Submit button in url mode", () => {
renderWithMantine(
<InlineElicitationRequest {...baseProps} request={urlRequest} />,
);
expect(
screen.queryByRole("button", { name: "Submit" }),
).not.toBeInTheDocument();
});
it("shows the waiting indicator when isWaiting is true", () => {
renderWithMantine(
<InlineElicitationRequest
{...baseProps}
request={urlRequest}
isWaiting
/>,
);
expect(screen.getByText("Waiting...")).toBeInTheDocument();
});
it("hides the waiting indicator when isWaiting is false", () => {
renderWithMantine(
<InlineElicitationRequest
{...baseProps}
request={urlRequest}
isWaiting={false}
/>,
);
expect(screen.queryByText("Waiting...")).not.toBeInTheDocument();
});
it("uses an empty values object when values prop is undefined", () => {
renderWithMantine(
<InlineElicitationRequest {...baseProps} request={formRequest} />,
);
expect(screen.getByText("Host")).toBeInTheDocument();
});
});

View file

@ -0,0 +1,215 @@
import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import type {
CreateMessageRequestParams,
CreateMessageResult,
} from "@modelcontextprotocol/sdk/types.js";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { InlineSamplingRequest } from "./InlineSamplingRequest";
const textRequest: CreateMessageRequestParams = {
messages: [
{
role: "user",
content: { type: "text", text: "Please analyze this code." },
},
],
maxTokens: 1024,
};
const arrayContentRequest: CreateMessageRequestParams = {
messages: [
{
role: "user",
content: [
// Note: SamplingMessage content is a single block, but the component
// also handles arrays defensively. Cast to satisfy types in tests.
{ type: "text", text: "from array" },
] as unknown as CreateMessageRequestParams["messages"][number]["content"],
},
],
maxTokens: 1024,
};
const nonTextSingleRequest: CreateMessageRequestParams = {
messages: [
{
role: "user",
content: {
type: "image",
data: "abc",
mimeType: "image/png",
},
},
],
maxTokens: 1024,
};
const emptyRequest: CreateMessageRequestParams = {
messages: [],
maxTokens: 1024,
};
const textDraft: CreateMessageResult = {
role: "assistant",
model: "claude-sonnet-4-20250514",
content: { type: "text", text: "draft response" },
};
const imageDraft: CreateMessageResult = {
role: "assistant",
model: "claude-sonnet-4-20250514",
content: { type: "image", data: "abc", mimeType: "image/png" },
};
const audioDraft: CreateMessageResult = {
role: "assistant",
model: "claude-sonnet-4-20250514",
content: { type: "audio", data: "abc", mimeType: "audio/wav" },
};
const baseProps = {
queuePosition: "1 of 1",
onAutoRespond: vi.fn(),
onEditAndSend: vi.fn(),
onReject: vi.fn(),
onViewDetails: vi.fn(),
};
describe("InlineSamplingRequest", () => {
it("renders the badge, queue position, and message preview", () => {
renderWithMantine(
<InlineSamplingRequest {...baseProps} request={textRequest} />,
);
expect(screen.getByText("sampling/createMessage")).toBeInTheDocument();
expect(screen.getByText("1 of 1")).toBeInTheDocument();
expect(screen.getByText("Please analyze this code.")).toBeInTheDocument();
});
it("renders model hints when provided", () => {
renderWithMantine(
<InlineSamplingRequest
{...baseProps}
request={{
...textRequest,
modelPreferences: {
hints: [{ name: "claude-opus" }, { name: "claude-sonnet" }],
},
}}
/>,
);
expect(
screen.getByText("Model hints: claude-opus, claude-sonnet"),
).toBeInTheDocument();
});
it("does not render hints when none are present", () => {
renderWithMantine(
<InlineSamplingRequest {...baseProps} request={textRequest} />,
);
expect(screen.queryByText(/^Model hints:/)).not.toBeInTheDocument();
});
it("does not render hints when hints array is empty", () => {
renderWithMantine(
<InlineSamplingRequest
{...baseProps}
request={{
...textRequest,
modelPreferences: { hints: [] },
}}
/>,
);
expect(screen.queryByText(/^Model hints:/)).not.toBeInTheDocument();
});
it("renders the text draft preview when a text draft result is provided", () => {
renderWithMantine(
<InlineSamplingRequest
{...baseProps}
request={textRequest}
draftResult={textDraft}
/>,
);
expect(screen.getByText("draft response")).toBeInTheDocument();
});
it("renders the image draft preview when an image draft result is provided", () => {
renderWithMantine(
<InlineSamplingRequest
{...baseProps}
request={textRequest}
draftResult={imageDraft}
/>,
);
expect(screen.getByText("[Image content]")).toBeInTheDocument();
});
it("renders the audio draft preview when an audio draft result is provided", () => {
renderWithMantine(
<InlineSamplingRequest
{...baseProps}
request={textRequest}
draftResult={audioDraft}
/>,
);
expect(screen.getByText("[Audio content]")).toBeInTheDocument();
});
it("handles requests with array-shaped content", () => {
renderWithMantine(
<InlineSamplingRequest {...baseProps} request={arrayContentRequest} />,
);
expect(screen.getByText("from array")).toBeInTheDocument();
});
it("renders a non-text content type label for non-text single-content messages", () => {
renderWithMantine(
<InlineSamplingRequest {...baseProps} request={nonTextSingleRequest} />,
);
expect(screen.getByText("[image]")).toBeInTheDocument();
});
it("handles requests with no messages", () => {
renderWithMantine(
<InlineSamplingRequest {...baseProps} request={emptyRequest} />,
);
expect(screen.getByText("sampling/createMessage")).toBeInTheDocument();
});
it("invokes onViewDetails when View Details is clicked", async () => {
const user = userEvent.setup();
const onViewDetails = vi.fn();
renderWithMantine(
<InlineSamplingRequest
{...baseProps}
request={textRequest}
onViewDetails={onViewDetails}
/>,
);
await user.click(screen.getByRole("button", { name: "View Details" }));
expect(onViewDetails).toHaveBeenCalledTimes(1);
});
it("invokes onAutoRespond, onEditAndSend, and onReject when their buttons are clicked", async () => {
const user = userEvent.setup();
const onAutoRespond = vi.fn();
const onEditAndSend = vi.fn();
const onReject = vi.fn();
renderWithMantine(
<InlineSamplingRequest
{...baseProps}
request={textRequest}
onAutoRespond={onAutoRespond}
onEditAndSend={onEditAndSend}
onReject={onReject}
/>,
);
await user.click(screen.getByRole("button", { name: "Auto-respond" }));
await user.click(screen.getByRole("button", { name: /Edit.*Send/ }));
await user.click(screen.getByRole("button", { name: "Reject" }));
expect(onAutoRespond).toHaveBeenCalledTimes(1);
expect(onEditAndSend).toHaveBeenCalledTimes(1);
expect(onReject).toHaveBeenCalledTimes(1);
});
});

View file

@ -0,0 +1,158 @@
import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import type { LoggingLevel } from "@modelcontextprotocol/sdk/types.js";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { LogControls } from "./LogControls";
const allVisible: Record<LoggingLevel, boolean> = {
debug: true,
info: true,
notice: true,
warning: true,
error: true,
critical: true,
alert: true,
emergency: true,
};
const someVisible: Record<LoggingLevel, boolean> = {
debug: false,
info: false,
notice: false,
warning: true,
error: true,
critical: true,
alert: true,
emergency: true,
};
const baseProps = {
currentLevel: "info" as LoggingLevel,
filterText: "",
visibleLevels: allVisible,
onSetLevel: vi.fn(),
onFilterChange: vi.fn(),
onToggleLevel: vi.fn(),
onToggleAllLevels: vi.fn(),
};
describe("LogControls", () => {
it("renders the title and search input", () => {
renderWithMantine(<LogControls {...baseProps} />);
expect(screen.getByText("Logging")).toBeInTheDocument();
expect(screen.getByPlaceholderText("Search...")).toBeInTheDocument();
expect(screen.getByText("Set Active Level")).toBeInTheDocument();
expect(screen.getByText("Filter by Level")).toBeInTheDocument();
});
it("renders a button for each log level", () => {
renderWithMantine(<LogControls {...baseProps} />);
expect(screen.getByRole("button", { name: "debug" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "info" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "notice" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "warning" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "error" })).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "critical" }),
).toBeInTheDocument();
expect(screen.getByRole("button", { name: "alert" })).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "emergency" }),
).toBeInTheDocument();
});
it("displays the current active level in the select", () => {
renderWithMantine(<LogControls {...baseProps} currentLevel="warning" />);
const inputs = screen.getAllByDisplayValue("warning");
expect(inputs.length).toBeGreaterThan(0);
});
it("invokes onFilterChange when typing in search", async () => {
const user = userEvent.setup();
const onFilterChange = vi.fn();
renderWithMantine(
<LogControls {...baseProps} onFilterChange={onFilterChange} />,
);
await user.type(screen.getByPlaceholderText("Search..."), "x");
expect(onFilterChange).toHaveBeenCalledWith("x");
});
it("invokes onSetLevel when Set is clicked", async () => {
const user = userEvent.setup();
const onSetLevel = vi.fn();
renderWithMantine(
<LogControls
{...baseProps}
currentLevel="warning"
onSetLevel={onSetLevel}
/>,
);
await user.click(screen.getByRole("button", { name: "Set" }));
expect(onSetLevel).toHaveBeenCalledWith("warning");
});
it("invokes onSetLevel when a level is selected from dropdown", async () => {
const user = userEvent.setup();
const onSetLevel = vi.fn();
renderWithMantine(<LogControls {...baseProps} onSetLevel={onSetLevel} />);
// Open the select dropdown
const inputs = screen.getAllByDisplayValue("info");
await user.click(inputs[0]);
const errorOption = await screen.findByRole("option", {
name: "error",
hidden: true,
});
await user.click(errorOption);
expect(onSetLevel).toHaveBeenCalledWith("error");
});
it("renders Deselect All when all levels are visible", () => {
renderWithMantine(<LogControls {...baseProps} />);
expect(
screen.getByRole("button", { name: "Deselect All" }),
).toBeInTheDocument();
});
it("renders Select All when not all levels are visible", () => {
renderWithMantine(
<LogControls {...baseProps} visibleLevels={someVisible} />,
);
expect(
screen.getByRole("button", { name: "Select All" }),
).toBeInTheDocument();
});
it("invokes onToggleAllLevels when toggle-all button is clicked", async () => {
const user = userEvent.setup();
const onToggleAllLevels = vi.fn();
renderWithMantine(
<LogControls {...baseProps} onToggleAllLevels={onToggleAllLevels} />,
);
await user.click(screen.getByRole("button", { name: "Deselect All" }));
expect(onToggleAllLevels).toHaveBeenCalledTimes(1);
});
it("invokes onToggleLevel when a level button is clicked", async () => {
const user = userEvent.setup();
const onToggleLevel = vi.fn();
renderWithMantine(
<LogControls {...baseProps} onToggleLevel={onToggleLevel} />,
);
await user.click(screen.getByRole("button", { name: "debug" }));
expect(onToggleLevel).toHaveBeenCalledWith("debug", false);
});
it("invokes onToggleLevel with true when an inactive level is clicked", async () => {
const user = userEvent.setup();
const onToggleLevel = vi.fn();
renderWithMantine(
<LogControls
{...baseProps}
visibleLevels={someVisible}
onToggleLevel={onToggleLevel}
/>,
);
await user.click(screen.getByRole("button", { name: "debug" }));
expect(onToggleLevel).toHaveBeenCalledWith("debug", true);
});
});

View file

@ -0,0 +1,175 @@
import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import type { LoggingLevel } from "@modelcontextprotocol/sdk/types.js";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { LogStreamPanel } from "./LogStreamPanel";
import type { LogEntryData } from "../../elements/LogEntry/LogEntry";
const allVisible: Record<LoggingLevel, boolean> = {
debug: true,
info: true,
notice: true,
warning: true,
error: true,
critical: true,
alert: true,
emergency: true,
};
const entries: LogEntryData[] = [
{
receivedAt: new Date("2026-03-17T10:00:00Z"),
params: { level: "info", data: "Server started", logger: "main" },
},
{
receivedAt: new Date("2026-03-17T10:00:01Z"),
params: { level: "error", data: "Failed to read", logger: "resources" },
},
{
receivedAt: new Date("2026-03-17T10:00:02Z"),
params: { level: "debug", data: "Loading config" },
},
{
receivedAt: new Date("2026-03-17T10:00:03Z"),
params: { level: "warning", data: { code: 42, msg: "deprecated" } },
},
{
receivedAt: new Date("2026-03-17T10:00:04Z"),
params: { level: "info", data: null },
},
];
const baseProps = {
entries,
filterText: "",
visibleLevels: allVisible,
autoScroll: true,
onToggleAutoScroll: vi.fn(),
onCopyAll: vi.fn(),
onClear: vi.fn(),
onExport: vi.fn(),
};
describe("LogStreamPanel", () => {
it("renders the title and toolbar buttons", () => {
renderWithMantine(<LogStreamPanel {...baseProps} />);
expect(screen.getByText("Log Stream")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Clear" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Export" })).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Copy All" }),
).toBeInTheDocument();
});
it("renders log entries when provided", () => {
renderWithMantine(<LogStreamPanel {...baseProps} />);
expect(screen.getByText("Server started")).toBeInTheDocument();
expect(screen.getByText("Failed to read")).toBeInTheDocument();
expect(screen.getByText("Loading config")).toBeInTheDocument();
});
it("serializes object data as JSON", () => {
renderWithMantine(<LogStreamPanel {...baseProps} />);
expect(
screen.getByText('{"code":42,"msg":"deprecated"}'),
).toBeInTheDocument();
});
it("renders empty state when there are no entries", () => {
renderWithMantine(<LogStreamPanel {...baseProps} entries={[]} />);
expect(screen.getByText("No log entries")).toBeInTheDocument();
});
it("renders empty state when all entries are filtered out", () => {
const hideAll: Record<LoggingLevel, boolean> = {
debug: false,
info: false,
notice: false,
warning: false,
error: false,
critical: false,
alert: false,
emergency: false,
};
renderWithMantine(
<LogStreamPanel {...baseProps} visibleLevels={hideAll} />,
);
expect(screen.getByText("No log entries")).toBeInTheDocument();
});
it("filters entries by visibleLevels", () => {
const onlyError: Record<LoggingLevel, boolean> = {
debug: false,
info: false,
notice: false,
warning: false,
error: true,
critical: false,
alert: false,
emergency: false,
};
renderWithMantine(
<LogStreamPanel {...baseProps} visibleLevels={onlyError} />,
);
expect(screen.getByText("Failed to read")).toBeInTheDocument();
expect(screen.queryByText("Server started")).not.toBeInTheDocument();
expect(screen.queryByText("Loading config")).not.toBeInTheDocument();
});
it("filters entries by filterText against the data", () => {
renderWithMantine(<LogStreamPanel {...baseProps} filterText="server" />);
expect(screen.getByText("Server started")).toBeInTheDocument();
expect(screen.queryByText("Failed to read")).not.toBeInTheDocument();
});
it("filters entries by filterText against the logger name", () => {
renderWithMantine(<LogStreamPanel {...baseProps} filterText="resources" />);
expect(screen.getByText("Failed to read")).toBeInTheDocument();
expect(screen.queryByText("Server started")).not.toBeInTheDocument();
});
it("filters entries by filterText against the level", () => {
renderWithMantine(<LogStreamPanel {...baseProps} filterText="debug" />);
expect(screen.getByText("Loading config")).toBeInTheDocument();
expect(screen.queryByText("Failed to read")).not.toBeInTheDocument();
});
it("invokes onClear when Clear is clicked", async () => {
const user = userEvent.setup();
const onClear = vi.fn();
renderWithMantine(<LogStreamPanel {...baseProps} onClear={onClear} />);
await user.click(screen.getByRole("button", { name: "Clear" }));
expect(onClear).toHaveBeenCalledTimes(1);
});
it("invokes onExport when Export is clicked", async () => {
const user = userEvent.setup();
const onExport = vi.fn();
renderWithMantine(<LogStreamPanel {...baseProps} onExport={onExport} />);
await user.click(screen.getByRole("button", { name: "Export" }));
expect(onExport).toHaveBeenCalledTimes(1);
});
it("invokes onCopyAll when Copy All is clicked", async () => {
const user = userEvent.setup();
const onCopyAll = vi.fn();
renderWithMantine(<LogStreamPanel {...baseProps} onCopyAll={onCopyAll} />);
await user.click(screen.getByRole("button", { name: "Copy All" }));
expect(onCopyAll).toHaveBeenCalledTimes(1);
});
it("invokes onToggleAutoScroll when the auto-scroll checkbox is clicked", async () => {
const user = userEvent.setup();
const onToggleAutoScroll = vi.fn();
renderWithMantine(
<LogStreamPanel {...baseProps} onToggleAutoScroll={onToggleAutoScroll} />,
);
await user.click(screen.getByLabelText("Auto-scroll"));
expect(onToggleAutoScroll).toHaveBeenCalledTimes(1);
});
it("renders auto-scroll checkbox with correct checked state", () => {
renderWithMantine(<LogStreamPanel {...baseProps} autoScroll={false} />);
expect(screen.getByLabelText("Auto-scroll")).not.toBeChecked();
});
});

View file

@ -0,0 +1,23 @@
import { describe, it, expect } from "vitest";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { PendingClientRequests } from "./PendingClientRequests";
describe("PendingClientRequests", () => {
it("renders the count in the title", () => {
renderWithMantine(
<PendingClientRequests count={3}>
<div>child</div>
</PendingClientRequests>,
);
expect(screen.getByText("Pending Client Requests (3)")).toBeInTheDocument();
});
it("renders the children", () => {
renderWithMantine(
<PendingClientRequests count={1}>
<div data-testid="child">child node</div>
</PendingClientRequests>,
);
expect(screen.getByTestId("child")).toBeInTheDocument();
});
});

View file

@ -0,0 +1,146 @@
import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import type { Prompt } from "@modelcontextprotocol/sdk/types.js";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { PromptArgumentsForm } from "./PromptArgumentsForm";
const promptNoArgs: Prompt = {
name: "summarize",
description: "Summarize the given text into key points",
};
const promptWithArgs: Prompt = {
name: "translate",
title: "Translate Text",
description: "Translate text from one language to another",
arguments: [
{ name: "text", required: true, description: "The text to translate" },
{
name: "targetLanguage",
required: false,
description: "The language to translate into",
},
],
};
const promptNoDescription: Prompt = {
name: "code-review",
arguments: [
{ name: "code", required: true, description: "The code to review" },
],
};
describe("PromptArgumentsForm", () => {
it("renders the prompt name when title is missing", () => {
renderWithMantine(
<PromptArgumentsForm
prompt={promptNoArgs}
argumentValues={{}}
onArgumentChange={vi.fn()}
onGetPrompt={vi.fn()}
/>,
);
expect(screen.getByText("summarize")).toBeInTheDocument();
expect(
screen.getByText("Summarize the given text into key points"),
).toBeInTheDocument();
});
it("prefers the prompt title over the name", () => {
renderWithMantine(
<PromptArgumentsForm
prompt={promptWithArgs}
argumentValues={{}}
onArgumentChange={vi.fn()}
onGetPrompt={vi.fn()}
/>,
);
expect(screen.getByText("Translate Text")).toBeInTheDocument();
});
it("does not render Arguments section when prompt has no arguments", () => {
renderWithMantine(
<PromptArgumentsForm
prompt={promptNoArgs}
argumentValues={{}}
onArgumentChange={vi.fn()}
onGetPrompt={vi.fn()}
/>,
);
expect(screen.queryByText("Arguments")).not.toBeInTheDocument();
});
it("renders argument inputs and the Arguments title when arguments are present", () => {
renderWithMantine(
<PromptArgumentsForm
prompt={promptWithArgs}
argumentValues={{}}
onArgumentChange={vi.fn()}
onGetPrompt={vi.fn()}
/>,
);
expect(screen.getByText("Arguments")).toBeInTheDocument();
expect(screen.getByLabelText(/text/)).toBeInTheDocument();
expect(screen.getByLabelText(/targetLanguage/)).toBeInTheDocument();
expect(screen.getByPlaceholderText("Enter text...")).toBeInTheDocument();
expect(
screen.getByPlaceholderText("Enter targetLanguage..."),
).toBeInTheDocument();
});
it("renders pre-filled argument values", () => {
renderWithMantine(
<PromptArgumentsForm
prompt={promptWithArgs}
argumentValues={{ text: "Hello", targetLanguage: "Spanish" }}
onArgumentChange={vi.fn()}
onGetPrompt={vi.fn()}
/>,
);
expect(screen.getByDisplayValue("Hello")).toBeInTheDocument();
expect(screen.getByDisplayValue("Spanish")).toBeInTheDocument();
});
it("invokes onArgumentChange when typing in an argument input", async () => {
const user = userEvent.setup();
const onArgumentChange = vi.fn();
renderWithMantine(
<PromptArgumentsForm
prompt={promptWithArgs}
argumentValues={{}}
onArgumentChange={onArgumentChange}
onGetPrompt={vi.fn()}
/>,
);
await user.type(screen.getByPlaceholderText("Enter text..."), "h");
expect(onArgumentChange).toHaveBeenCalledWith("text", "h");
});
it("invokes onGetPrompt when Get Prompt is clicked", async () => {
const user = userEvent.setup();
const onGetPrompt = vi.fn();
renderWithMantine(
<PromptArgumentsForm
prompt={promptWithArgs}
argumentValues={{}}
onArgumentChange={vi.fn()}
onGetPrompt={onGetPrompt}
/>,
);
await user.click(screen.getByRole("button", { name: "Get Prompt" }));
expect(onGetPrompt).toHaveBeenCalledTimes(1);
});
it("renders without description when none is provided", () => {
renderWithMantine(
<PromptArgumentsForm
prompt={promptNoDescription}
argumentValues={{}}
onArgumentChange={vi.fn()}
onGetPrompt={vi.fn()}
/>,
);
expect(screen.getByText("code-review")).toBeInTheDocument();
expect(screen.getByText("Arguments")).toBeInTheDocument();
});
});

View file

@ -0,0 +1,129 @@
import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import type { Prompt } from "@modelcontextprotocol/sdk/types.js";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { PromptControls } from "./PromptControls";
const samplePrompts: Prompt[] = [
{
name: "summarize",
description: "Summarize the given text into key points",
},
{
name: "translate",
description: "Translate text from one language to another",
},
{ name: "analyze", description: "Analyze sentiment and tone of the text" },
{ name: "code-review", description: "Review code for issues" },
{ name: "refactor" },
];
const baseProps = {
prompts: samplePrompts,
listChanged: false,
onRefreshList: vi.fn(),
onSelectPrompt: vi.fn(),
};
describe("PromptControls", () => {
it("renders the title and search input", () => {
renderWithMantine(<PromptControls {...baseProps} />);
expect(screen.getByText("Prompts")).toBeInTheDocument();
expect(
screen.getByPlaceholderText("Search prompts..."),
).toBeInTheDocument();
});
it("renders all prompts by default", () => {
renderWithMantine(<PromptControls {...baseProps} />);
expect(screen.getByText("summarize")).toBeInTheDocument();
expect(screen.getByText("translate")).toBeInTheDocument();
expect(screen.getByText("analyze")).toBeInTheDocument();
expect(screen.getByText("code-review")).toBeInTheDocument();
expect(screen.getByText("refactor")).toBeInTheDocument();
});
it("filters prompts by name when typing in the search input", async () => {
const user = userEvent.setup();
renderWithMantine(<PromptControls {...baseProps} />);
await user.type(screen.getByPlaceholderText("Search prompts..."), "trans");
expect(screen.getByText("translate")).toBeInTheDocument();
expect(screen.queryByText("summarize")).not.toBeInTheDocument();
expect(screen.queryByText("refactor")).not.toBeInTheDocument();
});
it("filters prompts by description when typing in the search input", async () => {
const user = userEvent.setup();
renderWithMantine(<PromptControls {...baseProps} />);
await user.type(
screen.getByPlaceholderText("Search prompts..."),
"sentiment",
);
expect(screen.getByText("analyze")).toBeInTheDocument();
expect(screen.queryByText("summarize")).not.toBeInTheDocument();
});
it("filters prompts by title when present", async () => {
const user = userEvent.setup();
const promptsWithTitle: Prompt[] = [
{ name: "p1", title: "Alpha Title" },
{ name: "p2", title: "Beta Title" },
];
renderWithMantine(
<PromptControls {...baseProps} prompts={promptsWithTitle} />,
);
await user.type(screen.getByPlaceholderText("Search prompts..."), "alpha");
expect(screen.getByText("Alpha Title")).toBeInTheDocument();
expect(screen.queryByText("Beta Title")).not.toBeInTheDocument();
});
it("invokes onSelectPrompt when an unselected prompt is clicked", async () => {
const user = userEvent.setup();
const onSelectPrompt = vi.fn();
renderWithMantine(
<PromptControls {...baseProps} onSelectPrompt={onSelectPrompt} />,
);
await user.click(screen.getByText("translate"));
expect(onSelectPrompt).toHaveBeenCalledWith("translate");
});
it("does not invoke onSelectPrompt when the already-selected prompt is clicked", async () => {
const user = userEvent.setup();
const onSelectPrompt = vi.fn();
renderWithMantine(
<PromptControls
{...baseProps}
selectedName="translate"
onSelectPrompt={onSelectPrompt}
/>,
);
await user.click(screen.getByText("translate"));
expect(onSelectPrompt).not.toHaveBeenCalled();
});
it("does not show the list-changed indicator when listChanged is false", () => {
renderWithMantine(<PromptControls {...baseProps} />);
expect(screen.queryByText("List updated")).not.toBeInTheDocument();
});
it("shows the list-changed indicator when listChanged is true and invokes onRefreshList", async () => {
const user = userEvent.setup();
const onRefreshList = vi.fn();
renderWithMantine(
<PromptControls
{...baseProps}
listChanged
onRefreshList={onRefreshList}
/>,
);
expect(screen.getByText("List updated")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Refresh" }));
expect(onRefreshList).toHaveBeenCalledTimes(1);
});
it("renders empty list when no prompts", () => {
renderWithMantine(<PromptControls {...baseProps} prompts={[]} />);
expect(screen.getByText("Prompts")).toBeInTheDocument();
expect(screen.queryByText("translate")).not.toBeInTheDocument();
});
});

View file

@ -0,0 +1,59 @@
import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import type { Prompt } from "@modelcontextprotocol/sdk/types.js";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { PromptListItem } from "./PromptListItem";
const prompt: Prompt = {
name: "summarize",
description: "Summarize a document",
};
describe("PromptListItem", () => {
it("renders the prompt name", () => {
renderWithMantine(
<PromptListItem prompt={prompt} selected={false} onClick={() => {}} />,
);
expect(screen.getByText("summarize")).toBeInTheDocument();
expect(screen.getByText("Summarize a document")).toBeInTheDocument();
});
it("prefers title over name when present", () => {
renderWithMantine(
<PromptListItem
prompt={{ ...prompt, title: "Summarize" }}
selected={false}
onClick={() => {}}
/>,
);
expect(screen.getByText("Summarize")).toBeInTheDocument();
});
it("omits the description when none is set", () => {
renderWithMantine(
<PromptListItem
prompt={{ name: "x" }}
selected={false}
onClick={() => {}}
/>,
);
expect(screen.queryByText("Summarize a document")).not.toBeInTheDocument();
});
it("invokes onClick when clicked", async () => {
const user = userEvent.setup();
const onClick = vi.fn();
renderWithMantine(
<PromptListItem prompt={prompt} selected={false} onClick={onClick} />,
);
await user.click(screen.getByRole("button"));
expect(onClick).toHaveBeenCalledTimes(1);
});
it("renders selected state", () => {
renderWithMantine(
<PromptListItem prompt={prompt} selected onClick={() => {}} />,
);
expect(screen.getByRole("button")).toBeInTheDocument();
});
});

View file

@ -0,0 +1,49 @@
import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import type { PromptMessage } from "@modelcontextprotocol/sdk/types.js";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { PromptMessagesDisplay } from "./PromptMessagesDisplay";
const messages: PromptMessage[] = [
{ role: "user", content: { type: "text", text: "hello" } },
{ role: "assistant", content: { type: "text", text: "hi there" } },
];
describe("PromptMessagesDisplay", () => {
it("renders the empty state when there are no messages", () => {
renderWithMantine(<PromptMessagesDisplay messages={[]} />);
expect(screen.getByText("No messages to display")).toBeInTheDocument();
});
it("renders all messages with role labels", () => {
renderWithMantine(<PromptMessagesDisplay messages={messages} />);
expect(screen.getByText("[0] role: user")).toBeInTheDocument();
expect(screen.getByText("[1] role: assistant")).toBeInTheDocument();
});
it("hides Copy All when no callback is provided", () => {
renderWithMantine(<PromptMessagesDisplay messages={messages} />);
expect(
screen.queryByRole("button", { name: "Copy All" }),
).not.toBeInTheDocument();
});
it("hides Copy All when there are no messages even with the callback", () => {
renderWithMantine(
<PromptMessagesDisplay messages={[]} onCopyAll={() => {}} />,
);
expect(
screen.queryByRole("button", { name: "Copy All" }),
).not.toBeInTheDocument();
});
it("invokes onCopyAll when Copy All is clicked", async () => {
const user = userEvent.setup();
const onCopyAll = vi.fn();
renderWithMantine(
<PromptMessagesDisplay messages={messages} onCopyAll={onCopyAll} />,
);
await user.click(screen.getByRole("button", { name: "Copy All" }));
expect(onCopyAll).toHaveBeenCalledTimes(1);
});
});

View file

@ -0,0 +1,184 @@
import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import type {
Resource,
ResourceTemplate,
} from "@modelcontextprotocol/sdk/types.js";
import type { InspectorResourceSubscription } from "@inspector/core/mcp/types.js";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { ResourceControls } from "./ResourceControls";
const sampleResources: Resource[] = [
{ name: "config.json", uri: "file:///config.json" },
{ name: "README.md", uri: "file:///README.md" },
];
const sampleTemplates: ResourceTemplate[] = [
{ name: "User Profile", uriTemplate: "file:///users/{userId}/profile" },
];
const sampleSubscriptions: InspectorResourceSubscription[] = [
{
resource: { name: "config.json", uri: "file:///config.json" },
lastUpdated: new Date("2026-03-17T10:30:00Z"),
},
];
const baseProps = {
resources: sampleResources,
templates: sampleTemplates,
subscriptions: sampleSubscriptions,
listChanged: false,
onRefreshList: vi.fn(),
onSelectUri: vi.fn(),
onSelectTemplate: vi.fn(),
onUnsubscribeResource: vi.fn(),
};
describe("ResourceControls", () => {
it("renders title and section counts", () => {
renderWithMantine(<ResourceControls {...baseProps} />);
expect(screen.getByText("Resources")).toBeInTheDocument();
expect(screen.getByText("URIs (2)")).toBeInTheDocument();
expect(screen.getByText("Templates (1)")).toBeInTheDocument();
expect(screen.getByText("Subscriptions (1)")).toBeInTheDocument();
});
it("does not show ListChangedIndicator when listChanged is false", () => {
renderWithMantine(<ResourceControls {...baseProps} />);
expect(screen.queryByText("List updated")).not.toBeInTheDocument();
});
it("shows ListChangedIndicator and triggers onRefreshList when listChanged", async () => {
const user = userEvent.setup();
const onRefreshList = vi.fn();
renderWithMantine(
<ResourceControls
{...baseProps}
listChanged
onRefreshList={onRefreshList}
/>,
);
expect(screen.getByText("List updated")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Refresh" }));
expect(onRefreshList).toHaveBeenCalledTimes(1);
});
it("filters resources, templates, and subscriptions by search text", async () => {
const user = userEvent.setup();
renderWithMantine(<ResourceControls {...baseProps} />);
await user.type(screen.getByPlaceholderText("Search..."), "README");
expect(screen.getByText("URIs (1)")).toBeInTheDocument();
expect(screen.getByText("Templates (0)")).toBeInTheDocument();
expect(screen.getByText("Subscriptions (0)")).toBeInTheDocument();
});
it("invokes onSelectUri when a resource is clicked", async () => {
const user = userEvent.setup();
const onSelectUri = vi.fn();
renderWithMantine(
<ResourceControls {...baseProps} onSelectUri={onSelectUri} />,
);
await user.click(screen.getByText("README.md"));
expect(onSelectUri).toHaveBeenCalledWith("file:///README.md");
});
it("does not invoke onSelectUri when clicking the already-selected resource", async () => {
const user = userEvent.setup();
const onSelectUri = vi.fn();
renderWithMantine(
<ResourceControls
{...baseProps}
selectedUri="file:///README.md"
onSelectUri={onSelectUri}
/>,
);
await user.click(screen.getByText("README.md"));
expect(onSelectUri).not.toHaveBeenCalled();
});
it("invokes onSelectTemplate when a template is clicked", async () => {
const user = userEvent.setup();
const onSelectTemplate = vi.fn();
renderWithMantine(
<ResourceControls {...baseProps} onSelectTemplate={onSelectTemplate} />,
);
await user.click(screen.getByText("User Profile"));
expect(onSelectTemplate).toHaveBeenCalledWith(
"file:///users/{userId}/profile",
);
});
it("does not invoke onSelectTemplate when clicking the already-selected template", async () => {
const user = userEvent.setup();
const onSelectTemplate = vi.fn();
renderWithMantine(
<ResourceControls
{...baseProps}
selectedTemplateUri="file:///users/{userId}/profile"
onSelectTemplate={onSelectTemplate}
/>,
);
await user.click(screen.getByText("User Profile"));
expect(onSelectTemplate).not.toHaveBeenCalled();
});
it("invokes onUnsubscribeResource when Unsubscribe is clicked", async () => {
const user = userEvent.setup();
const onUnsubscribeResource = vi.fn();
renderWithMantine(
<ResourceControls
{...baseProps}
onUnsubscribeResource={onUnsubscribeResource}
/>,
);
await user.click(screen.getByRole("button", { name: "Unsubscribe" }));
expect(onUnsubscribeResource).toHaveBeenCalledWith("file:///config.json");
});
it("renders empty section counts when collections are empty", () => {
renderWithMantine(
<ResourceControls
{...baseProps}
resources={[]}
templates={[]}
subscriptions={[]}
/>,
);
expect(screen.getByText("URIs (0)")).toBeInTheDocument();
expect(screen.getByText("Templates (0)")).toBeInTheDocument();
expect(screen.getByText("Subscriptions (0)")).toBeInTheDocument();
});
it("toggles all sections via the ListToggle button", async () => {
const user = userEvent.setup();
renderWithMantine(<ResourceControls {...baseProps} />);
const toggleButtons = screen.getAllByRole("button");
// The list toggle is the last in the search row; just exercise it.
// Since all three sections start open, clicking it should collapse them.
const toggle = toggleButtons.find(
(btn) => btn.querySelector("svg") && btn !== toggleButtons[0],
);
if (toggle) {
await user.click(toggle);
}
// After collapse, click again to re-expand.
if (toggle) {
await user.click(toggle);
}
expect(screen.getByText("URIs (2)")).toBeInTheDocument();
});
it("filters by resource title when title is set", async () => {
const user = userEvent.setup();
const resourcesWithTitle: Resource[] = [
{ name: "x", title: "Special Title", uri: "file:///x" },
{ name: "y", uri: "file:///y" },
];
renderWithMantine(
<ResourceControls {...baseProps} resources={resourcesWithTitle} />,
);
await user.type(screen.getByPlaceholderText("Search..."), "special");
expect(screen.getByText("URIs (1)")).toBeInTheDocument();
});
});

View file

@ -0,0 +1,71 @@
import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import type { Resource } from "@modelcontextprotocol/sdk/types.js";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { ResourceListItem } from "./ResourceListItem";
const baseResource: Resource = { uri: "file:///x", name: "config.json" };
describe("ResourceListItem", () => {
it("renders the resource name", () => {
renderWithMantine(
<ResourceListItem
resource={baseResource}
selected={false}
onClick={() => {}}
/>,
);
expect(screen.getByText("config.json")).toBeInTheDocument();
});
it("prefers the resource title", () => {
renderWithMantine(
<ResourceListItem
resource={{ ...baseResource, title: "Configuration" }}
selected={false}
onClick={() => {}}
/>,
);
expect(screen.getByText("Configuration")).toBeInTheDocument();
});
it("renders annotation badges when annotations are present", () => {
renderWithMantine(
<ResourceListItem
resource={{
...baseResource,
annotations: { audience: ["user"], priority: 0.9 },
}}
selected={false}
onClick={() => {}}
/>,
);
expect(screen.getByText("audience: user")).toBeInTheDocument();
expect(screen.getByText("priority: high")).toBeInTheDocument();
});
it("hides badges when only an empty audience annotation exists", () => {
renderWithMantine(
<ResourceListItem
resource={{ ...baseResource, annotations: { audience: [] } }}
selected={false}
onClick={() => {}}
/>,
);
expect(screen.queryByText(/audience/)).not.toBeInTheDocument();
});
it("invokes onClick when the row is clicked", async () => {
const user = userEvent.setup();
const onClick = vi.fn();
renderWithMantine(
<ResourceListItem
resource={baseResource}
selected={false}
onClick={onClick}
/>,
);
await user.click(screen.getByRole("button"));
expect(onClick).toHaveBeenCalledTimes(1);
});
});

View file

@ -0,0 +1,202 @@
import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import type {
BlobResourceContents,
Resource,
TextResourceContents,
} from "@modelcontextprotocol/sdk/types.js";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { ResourcePreviewPanel } from "./ResourcePreviewPanel";
const textResource: Resource = {
name: "config.json",
uri: "file:///config.json",
};
const textContents: TextResourceContents[] = [
{
uri: "file:///config.json",
mimeType: "application/json",
text: '{"a":1}',
},
];
const imageBlob: BlobResourceContents = {
uri: "file:///x.png",
mimeType: "image/png",
blob: "abc",
};
const audioBlob: BlobResourceContents = {
uri: "file:///x.wav",
mimeType: "audio/wav",
blob: "abc",
};
const otherBlob: BlobResourceContents = {
uri: "file:///x.bin",
mimeType: "application/octet-stream",
blob: "abc",
};
const blobNoMime: BlobResourceContents = {
uri: "file:///x",
blob: "abc",
};
const baseProps = {
resource: textResource,
contents: textContents,
isSubscribed: false,
onRefresh: vi.fn(),
onSubscribe: vi.fn(),
onUnsubscribe: vi.fn(),
};
describe("ResourcePreviewPanel", () => {
it("renders the resource title and URI", () => {
renderWithMantine(<ResourcePreviewPanel {...baseProps} />);
expect(screen.getByText("Resource")).toBeInTheDocument();
expect(screen.getByText("file:///config.json")).toBeInTheDocument();
});
it("renders the mimeType when contents has at most one item", () => {
renderWithMantine(<ResourcePreviewPanel {...baseProps} />);
expect(screen.getByText("application/json")).toBeInTheDocument();
});
it("does not render mimeType when there are multiple content items", () => {
renderWithMantine(
<ResourcePreviewPanel
{...baseProps}
contents={[textContents[0], textContents[0]]}
/>,
);
expect(screen.queryByText("application/json")).not.toBeInTheDocument();
});
it("renders the lastUpdated timestamp when provided", () => {
renderWithMantine(
<ResourcePreviewPanel
{...baseProps}
lastUpdated={new Date("2026-03-17T10:30:00Z")}
/>,
);
expect(screen.getByText(/^Last updated:/)).toBeInTheDocument();
});
it("renders Subscribe button when not subscribed and triggers onSubscribe", async () => {
const user = userEvent.setup();
const onSubscribe = vi.fn();
renderWithMantine(
<ResourcePreviewPanel {...baseProps} onSubscribe={onSubscribe} />,
);
await user.click(screen.getByRole("button", { name: "Subscribe" }));
expect(onSubscribe).toHaveBeenCalledTimes(1);
});
it("renders Unsubscribe button when subscribed and triggers onUnsubscribe", async () => {
const user = userEvent.setup();
const onUnsubscribe = vi.fn();
renderWithMantine(
<ResourcePreviewPanel
{...baseProps}
isSubscribed
onUnsubscribe={onUnsubscribe}
/>,
);
await user.click(screen.getByRole("button", { name: "Unsubscribe" }));
expect(onUnsubscribe).toHaveBeenCalledTimes(1);
});
it("invokes onRefresh when Refresh is clicked", async () => {
const user = userEvent.setup();
const onRefresh = vi.fn();
renderWithMantine(
<ResourcePreviewPanel {...baseProps} onRefresh={onRefresh} />,
);
await user.click(screen.getByRole("button", { name: "Refresh" }));
expect(onRefresh).toHaveBeenCalledTimes(1);
});
it("renders annotation badges when present", () => {
renderWithMantine(
<ResourcePreviewPanel
{...baseProps}
resource={{
...textResource,
annotations: { audience: ["user"], priority: 0.8 },
}}
/>,
);
expect(screen.getByText("audience: user")).toBeInTheDocument();
expect(screen.getByText("priority: high")).toBeInTheDocument();
});
it("renders an image content viewer for image blobs", () => {
renderWithMantine(
<ResourcePreviewPanel
{...baseProps}
resource={{ name: "img", uri: "file:///x.png" }}
contents={[imageBlob]}
/>,
);
const img = screen
.getAllByRole("img")
.find((el) => el.getAttribute("src")?.startsWith("data:image/png"));
expect(img).toBeDefined();
});
it("renders an audio element for audio blobs", () => {
const { container } = renderWithMantine(
<ResourcePreviewPanel
{...baseProps}
resource={{ name: "snd", uri: "file:///x.wav" }}
contents={[audioBlob]}
/>,
);
const audio = container.querySelector("audio");
expect(audio).not.toBeNull();
});
it("renders a textual unsupported message for non-image, non-audio blobs", () => {
renderWithMantine(
<ResourcePreviewPanel
{...baseProps}
resource={{ name: "bin", uri: "file:///x.bin" }}
contents={[otherBlob]}
/>,
);
expect(
screen.getByText(
"[Binary content (application/octet-stream) — preview not supported]",
),
).toBeInTheDocument();
});
it("falls back to application/octet-stream when blob has no mimeType", () => {
renderWithMantine(
<ResourcePreviewPanel
{...baseProps}
resource={{ name: "bin", uri: "file:///x" }}
contents={[blobNoMime]}
/>,
);
expect(screen.getByText("application/octet-stream")).toBeInTheDocument();
});
it("falls back to resource.mimeType when contents is empty", () => {
renderWithMantine(
<ResourcePreviewPanel
{...baseProps}
resource={{
name: "x",
uri: "file:///x",
mimeType: "text/markdown",
}}
contents={[]}
/>,
);
expect(screen.getByText("text/markdown")).toBeInTheDocument();
});
});

View file

@ -0,0 +1,59 @@
import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import type { InspectorResourceSubscription } from "@inspector/core/mcp/types.js";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { ResourceSubscribedItem } from "./ResourceSubscribedItem";
const subscription: InspectorResourceSubscription = {
resource: { uri: "file:///x", name: "x" },
lastUpdated: new Date("2024-01-01T12:00:00Z"),
};
describe("ResourceSubscribedItem", () => {
it("renders the resource name", () => {
renderWithMantine(
<ResourceSubscribedItem
subscription={{ resource: subscription.resource }}
onUnsubscribe={() => {}}
/>,
);
expect(screen.getByText("x")).toBeInTheDocument();
});
it("prefers the resource title over the name", () => {
renderWithMantine(
<ResourceSubscribedItem
subscription={{
resource: { ...subscription.resource, title: "Display X" },
}}
onUnsubscribe={() => {}}
/>,
);
expect(screen.getByText("Display X")).toBeInTheDocument();
});
it("renders the last updated timestamp when present", () => {
renderWithMantine(
<ResourceSubscribedItem
subscription={subscription}
onUnsubscribe={() => {}}
/>,
);
expect(
screen.getByText(subscription.lastUpdated!.toLocaleString()),
).toBeInTheDocument();
});
it("invokes onUnsubscribe when the button is clicked", async () => {
const user = userEvent.setup();
const onUnsubscribe = vi.fn();
renderWithMantine(
<ResourceSubscribedItem
subscription={subscription}
onUnsubscribe={onUnsubscribe}
/>,
);
await user.click(screen.getByRole("button", { name: "Unsubscribe" }));
expect(onUnsubscribe).toHaveBeenCalledTimes(1);
});
});

Some files were not shown because too many files have changed in this diff Show more