import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import type { Root } from "@modelcontextprotocol/client";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { RootsTable } from "./RootsTable";
const sampleRoots: Root[] = [
{ name: "Project Source", uri: "file:///home/user/project/src" },
{ name: "Configuration", uri: "file:///home/user/project/config" },
];
const baseProps = {
roots: sampleRoots,
newRootDraft: { name: "", uri: "" },
onRemoveRoot: vi.fn(),
onNewRootDraftChange: vi.fn(),
onAddRoot: vi.fn(),
onBrowse: vi.fn(),
};
describe("RootsTable", () => {
it("renders the title, hint, and warning alert", () => {
renderWithMantine();
expect(screen.getByText("Roots Configuration")).toBeInTheDocument();
expect(
screen.getByText("Filesystem roots exposed to the connected server:"),
).toBeInTheDocument();
expect(screen.getByText("Warning")).toBeInTheDocument();
});
it("renders the roots table with provided roots", () => {
renderWithMantine();
expect(screen.getByText("Project Source")).toBeInTheDocument();
expect(
screen.getByText("file:///home/user/project/src"),
).toBeInTheDocument();
expect(screen.getByText("Configuration")).toBeInTheDocument();
});
it("does not render the table when roots is empty", () => {
renderWithMantine();
expect(screen.queryByRole("table")).not.toBeInTheDocument();
});
it("invokes onRemoveRoot when an X icon is clicked", async () => {
const user = userEvent.setup();
const onRemoveRoot = vi.fn();
renderWithMantine(
,
);
const removes = screen.getAllByRole("button", { name: "X" });
await user.click(removes[0]);
expect(onRemoveRoot).toHaveBeenCalledWith("file:///home/user/project/src");
});
it("invokes onAddRoot when the + Add Root button is clicked", async () => {
const user = userEvent.setup();
const onAddRoot = vi.fn();
renderWithMantine();
await user.click(screen.getByRole("button", { name: "+ Add Root" }));
expect(onAddRoot).toHaveBeenCalledTimes(1);
});
it("invokes onAddRoot when the bottom Add button is clicked", async () => {
const user = userEvent.setup();
const onAddRoot = vi.fn();
renderWithMantine();
await user.click(screen.getByRole("button", { name: "Add" }));
expect(onAddRoot).toHaveBeenCalledTimes(1);
});
it("invokes onBrowse when Browse is clicked", async () => {
const user = userEvent.setup();
const onBrowse = vi.fn();
renderWithMantine();
await user.click(screen.getByRole("button", { name: "Browse" }));
expect(onBrowse).toHaveBeenCalledTimes(1);
});
it("invokes onNewRootDraftChange when typing into the Name input", async () => {
const user = userEvent.setup();
const onNewRootDraftChange = vi.fn();
renderWithMantine(
,
);
await user.type(screen.getByLabelText("Name"), "x");
expect(onNewRootDraftChange).toHaveBeenCalledWith({ name: "x", uri: "" });
});
it("invokes onNewRootDraftChange when typing into the URI input", async () => {
const user = userEvent.setup();
const onNewRootDraftChange = vi.fn();
renderWithMantine(
,
);
await user.type(screen.getByLabelText("URI"), "y");
expect(onNewRootDraftChange).toHaveBeenCalledWith({ name: "", uri: "y" });
});
it("clears the Name and URI inputs via their Clear buttons", async () => {
const user = userEvent.setup();
const onNewRootDraftChange = vi.fn();
renderWithMantine(
,
);
const clearButtons = screen.getAllByRole("button", { name: "Clear" });
expect(clearButtons).toHaveLength(2);
await user.click(clearButtons[0]);
expect(onNewRootDraftChange).toHaveBeenCalledWith({ name: "", uri: "u" });
await user.click(clearButtons[1]);
expect(onNewRootDraftChange).toHaveBeenCalledWith({ name: "n", uri: "" });
});
it("renders no Clear buttons when the draft fields are empty", () => {
renderWithMantine();
expect(
screen.queryByRole("button", { name: "Clear" }),
).not.toBeInTheDocument();
});
// The roots list is user-maintained and nothing dedupes it, so the same URI
// can appear twice and collide on the row key (#2206).
it("renders both rows when a root URI repeats (#2206)", () => {
const consoleError = vi
.spyOn(console, "error")
.mockImplementation(() => {});
try {
renderWithMantine(
,
);
expect(screen.getByText("First")).toBeInTheDocument();
expect(screen.getByText("Second")).toBeInTheDocument();
const messages = consoleError.mock.calls.map((call) =>
call.map(String).join(" "),
);
expect(
messages.filter((message) => message.includes("same key")),
).toEqual([]);
} finally {
consoleError.mockRestore();
}
});
it("renders the current draft values in the inputs", () => {
renderWithMantine(
,
);
expect(screen.getByDisplayValue("Test")).toBeInTheDocument();
expect(screen.getByDisplayValue("file:///t")).toBeInTheDocument();
});
});