import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import type { Tool } from "@modelcontextprotocol/client";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { setAceTextByLabel } from "../../../test/aceEditor";
import { AppDetailPanel } from "./AppDetailPanel";
const noFieldsTool: Tool = {
name: "no_input_app",
title: "No Input App",
description: "Takes no parameters",
inputSchema: { type: "object" },
};
const requiredFieldTool: Tool = {
name: "greet",
title: "Greet",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "The name to greet" },
},
required: ["name"],
},
};
const baseProps = {
formValues: {},
isOpening: false,
onFormChange: vi.fn(),
onOpenApp: vi.fn(),
};
describe("AppDetailPanel", () => {
it("renders the description when provided", () => {
renderWithMantine();
expect(screen.getByText("Takes no parameters")).toBeInTheDocument();
});
it("does not render the description when missing", () => {
renderWithMantine(
,
);
expect(screen.queryByText("Takes no parameters")).not.toBeInTheDocument();
});
it("renders the schema form using the tool's inputSchema", () => {
renderWithMantine(
,
);
expect(screen.getByText("The name to greet")).toBeInTheDocument();
});
it("invokes onFormChange when the user types in a form field", async () => {
const user = userEvent.setup();
const onFormChange = vi.fn();
renderWithMantine(
,
);
await user.type(screen.getByRole("textbox"), "h");
expect(onFormChange).toHaveBeenCalled();
});
it("renders a divider above the form when the schema has properties", () => {
renderWithMantine(
,
);
expect(screen.getByRole("separator")).toBeInTheDocument();
});
it("omits the divider when the schema has no properties", () => {
renderWithMantine();
expect(screen.queryByRole("separator")).not.toBeInTheDocument();
});
it("disables the Open App button when a required field is empty", () => {
renderWithMantine(
,
);
expect(screen.getByRole("button", { name: /open app/i })).toBeDisabled();
});
it("enables the Open App button when required fields are populated", () => {
renderWithMantine(
,
);
expect(
screen.getByRole("button", { name: /open app/i }),
).not.toBeDisabled();
});
it("enables the Open App button when there are no required fields", () => {
renderWithMantine();
expect(
screen.getByRole("button", { name: /open app/i }),
).not.toBeDisabled();
});
it("treats null and empty-string values as missing required fields", () => {
const { rerender } = renderWithMantine(
,
);
expect(screen.getByRole("button", { name: /open app/i })).toBeDisabled();
rerender(
,
);
expect(screen.getByRole("button", { name: /open app/i })).toBeDisabled();
});
it("disables the Open App button while opening", () => {
renderWithMantine(
,
);
expect(screen.getByRole("button", { name: /open app/i })).toBeDisabled();
});
it("invokes onOpenApp when the button is clicked", async () => {
const user = userEvent.setup();
const onOpenApp = vi.fn();
renderWithMantine(
,
);
await user.click(screen.getByRole("button", { name: /open app/i }));
expect(onOpenApp).toHaveBeenCalledTimes(1);
});
it("drops a previous app's in-progress number text when the app changes", async () => {
const user = userEvent.setup();
// AppsScreen swaps selectedAppName + formValues in place rather than
// remounting this panel, so two apps exposing a same-named number field
// share the field component. Both values here are undefined, which the
// draft/value re-sync cannot distinguish — only the resetKey identity can.
const numberFieldTool = (name: string): Tool => ({
name,
title: name,
inputSchema: {
type: "object",
properties: { scale: { type: "number", title: "Scale" } },
},
});
const { rerender } = renderWithMantine(
,
);
const input = () => screen.getByLabelText(/Scale/) as HTMLInputElement;
await user.type(input(), "-");
expect(input().value).toBe("-");
rerender();
expect(input().value).toBe("");
});
// #2020: `hasMissingRequiredFields` sees a field with no value, but text a
// field could not turn into a value looks identical to it — both report
// `undefined` — so an optional argument was dropped from the call with no
// gate. The form reports draft validity for this reason.
it("disables Open App while a field holds text it cannot send", async () => {
const jsonFieldTool: Tool = {
name: "chart_app",
title: "Chart App",
inputSchema: {
type: "object",
properties: { series: { type: "array", title: "Series" } },
},
};
renderWithMantine();
const openApp = screen.getByRole("button", { name: /open app/i });
expect(openApp).not.toBeDisabled();
await setAceTextByLabel(/Series/, "x");
expect(openApp).toBeDisabled();
await setAceTextByLabel(/Series/, "");
expect(openApp).not.toBeDisabled();
});
});