626 lines
19 KiB
TypeScript
626 lines
19 KiB
TypeScript
import { render, screen, fireEvent } from "@testing-library/react";
|
||
import "@testing-library/jest-dom";
|
||
import { describe, it, beforeEach, jest } from "@jest/globals";
|
||
import Sidebar from "../Sidebar";
|
||
import { DEFAULT_INSPECTOR_CONFIG } from "@/lib/constants";
|
||
import { InspectorConfig } from "@/lib/configurationTypes";
|
||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||
|
||
// Mock theme hook
|
||
jest.mock("../../lib/useTheme", () => ({
|
||
__esModule: true,
|
||
default: () => ["light", jest.fn()],
|
||
}));
|
||
|
||
describe("Sidebar Environment Variables", () => {
|
||
const defaultProps = {
|
||
connectionStatus: "disconnected" as const,
|
||
transportType: "stdio" as const,
|
||
setTransportType: jest.fn(),
|
||
command: "",
|
||
setCommand: jest.fn(),
|
||
args: "",
|
||
setArgs: jest.fn(),
|
||
sseUrl: "",
|
||
setSseUrl: jest.fn(),
|
||
env: {},
|
||
setEnv: jest.fn(),
|
||
bearerToken: "",
|
||
setBearerToken: jest.fn(),
|
||
onConnect: jest.fn(),
|
||
onDisconnect: jest.fn(),
|
||
stdErrNotifications: [],
|
||
clearStdErrNotifications: jest.fn(),
|
||
logLevel: "info" as const,
|
||
sendLogLevelRequest: jest.fn(),
|
||
loggingSupported: true,
|
||
config: DEFAULT_INSPECTOR_CONFIG,
|
||
setConfig: jest.fn(),
|
||
};
|
||
|
||
const renderSidebar = (props = {}) => {
|
||
return render(
|
||
<TooltipProvider>
|
||
<Sidebar {...defaultProps} {...props} />
|
||
</TooltipProvider>,
|
||
);
|
||
};
|
||
|
||
const openEnvVarsSection = () => {
|
||
const button = screen.getByTestId("env-vars-button");
|
||
fireEvent.click(button);
|
||
};
|
||
|
||
beforeEach(() => {
|
||
jest.clearAllMocks();
|
||
});
|
||
|
||
describe("Basic Operations", () => {
|
||
it("should add a new environment variable", () => {
|
||
const setEnv = jest.fn();
|
||
renderSidebar({ env: {}, setEnv });
|
||
|
||
openEnvVarsSection();
|
||
|
||
const addButton = screen.getByText("Add Environment Variable");
|
||
fireEvent.click(addButton);
|
||
|
||
expect(setEnv).toHaveBeenCalledWith({ "": "" });
|
||
});
|
||
|
||
it("should remove an environment variable", () => {
|
||
const setEnv = jest.fn();
|
||
const initialEnv = { TEST_KEY: "test_value" };
|
||
renderSidebar({ env: initialEnv, setEnv });
|
||
|
||
openEnvVarsSection();
|
||
|
||
const removeButton = screen.getByRole("button", { name: "×" });
|
||
fireEvent.click(removeButton);
|
||
|
||
expect(setEnv).toHaveBeenCalledWith({});
|
||
});
|
||
|
||
it("should update environment variable value", () => {
|
||
const setEnv = jest.fn();
|
||
const initialEnv = { TEST_KEY: "test_value" };
|
||
renderSidebar({ env: initialEnv, setEnv });
|
||
|
||
openEnvVarsSection();
|
||
|
||
const valueInput = screen.getByDisplayValue("test_value");
|
||
fireEvent.change(valueInput, { target: { value: "new_value" } });
|
||
|
||
expect(setEnv).toHaveBeenCalledWith({ TEST_KEY: "new_value" });
|
||
});
|
||
|
||
it("should toggle value visibility", () => {
|
||
const initialEnv = { TEST_KEY: "test_value" };
|
||
renderSidebar({ env: initialEnv });
|
||
|
||
openEnvVarsSection();
|
||
|
||
const valueInput = screen.getByDisplayValue("test_value");
|
||
expect(valueInput).toHaveProperty("type", "password");
|
||
|
||
const toggleButton = screen.getByRole("button", { name: /show value/i });
|
||
fireEvent.click(toggleButton);
|
||
|
||
expect(valueInput).toHaveProperty("type", "text");
|
||
});
|
||
});
|
||
|
||
describe("Authentication", () => {
|
||
const openAuthSection = () => {
|
||
const button = screen.getByTestId("auth-button");
|
||
fireEvent.click(button);
|
||
};
|
||
|
||
it("should update bearer token", () => {
|
||
const setBearerToken = jest.fn();
|
||
renderSidebar({
|
||
bearerToken: "",
|
||
setBearerToken,
|
||
transportType: "sse", // Set transport type to SSE
|
||
});
|
||
|
||
openAuthSection();
|
||
|
||
const tokenInput = screen.getByTestId("bearer-token-input");
|
||
fireEvent.change(tokenInput, { target: { value: "new_token" } });
|
||
|
||
expect(setBearerToken).toHaveBeenCalledWith("new_token");
|
||
});
|
||
|
||
it("should update header name", () => {
|
||
const setHeaderName = jest.fn();
|
||
renderSidebar({
|
||
headerName: "Authorization",
|
||
setHeaderName,
|
||
transportType: "sse",
|
||
});
|
||
|
||
openAuthSection();
|
||
|
||
const headerInput = screen.getByTestId("header-input");
|
||
fireEvent.change(headerInput, { target: { value: "X-Custom-Auth" } });
|
||
|
||
expect(setHeaderName).toHaveBeenCalledWith("X-Custom-Auth");
|
||
});
|
||
|
||
it("should clear bearer token", () => {
|
||
const setBearerToken = jest.fn();
|
||
renderSidebar({
|
||
bearerToken: "existing_token",
|
||
setBearerToken,
|
||
transportType: "sse", // Set transport type to SSE
|
||
});
|
||
|
||
openAuthSection();
|
||
|
||
const tokenInput = screen.getByTestId("bearer-token-input");
|
||
fireEvent.change(tokenInput, { target: { value: "" } });
|
||
|
||
expect(setBearerToken).toHaveBeenCalledWith("");
|
||
});
|
||
|
||
it("should properly render bearer token input", () => {
|
||
const { rerender } = renderSidebar({
|
||
bearerToken: "existing_token",
|
||
transportType: "sse", // Set transport type to SSE
|
||
});
|
||
|
||
openAuthSection();
|
||
|
||
// Token input should be a password field
|
||
const tokenInput = screen.getByTestId("bearer-token-input");
|
||
expect(tokenInput).toHaveProperty("type", "password");
|
||
|
||
// Update the token
|
||
fireEvent.change(tokenInput, { target: { value: "new_token" } });
|
||
|
||
// Rerender with updated token
|
||
rerender(
|
||
<TooltipProvider>
|
||
<Sidebar
|
||
{...defaultProps}
|
||
bearerToken="new_token"
|
||
transportType="sse"
|
||
/>
|
||
</TooltipProvider>,
|
||
);
|
||
|
||
// Token input should still exist after update
|
||
expect(screen.getByTestId("bearer-token-input")).toBeInTheDocument();
|
||
});
|
||
|
||
it("should maintain token visibility state after update", () => {
|
||
const { rerender } = renderSidebar({
|
||
bearerToken: "existing_token",
|
||
transportType: "sse", // Set transport type to SSE
|
||
});
|
||
|
||
openAuthSection();
|
||
|
||
// Token input should be a password field
|
||
const tokenInput = screen.getByTestId("bearer-token-input");
|
||
expect(tokenInput).toHaveProperty("type", "password");
|
||
|
||
// Update the token
|
||
fireEvent.change(tokenInput, { target: { value: "new_token" } });
|
||
|
||
// Rerender with updated token
|
||
rerender(
|
||
<TooltipProvider>
|
||
<Sidebar
|
||
{...defaultProps}
|
||
bearerToken="new_token"
|
||
transportType="sse"
|
||
/>
|
||
</TooltipProvider>,
|
||
);
|
||
|
||
// Token input should still exist after update
|
||
expect(screen.getByTestId("bearer-token-input")).toBeInTheDocument();
|
||
});
|
||
|
||
it("should maintain header name when toggling auth section", () => {
|
||
renderSidebar({
|
||
headerName: "X-API-Key",
|
||
transportType: "sse",
|
||
});
|
||
|
||
// Open auth section
|
||
openAuthSection();
|
||
|
||
// Verify header name is displayed
|
||
const headerInput = screen.getByTestId("header-input");
|
||
expect(headerInput).toHaveValue("X-API-Key");
|
||
|
||
// Close auth section
|
||
const authButton = screen.getByTestId("auth-button");
|
||
fireEvent.click(authButton);
|
||
|
||
// Reopen auth section
|
||
fireEvent.click(authButton);
|
||
|
||
// Verify header name is still preserved
|
||
expect(screen.getByTestId("header-input")).toHaveValue("X-API-Key");
|
||
});
|
||
|
||
it("should display default header name when not specified", () => {
|
||
renderSidebar({
|
||
headerName: undefined,
|
||
transportType: "sse",
|
||
});
|
||
|
||
openAuthSection();
|
||
|
||
const headerInput = screen.getByTestId("header-input");
|
||
expect(headerInput).toHaveAttribute("placeholder", "Authorization");
|
||
});
|
||
});
|
||
|
||
describe("Key Editing", () => {
|
||
it("should maintain order when editing first key", () => {
|
||
const setEnv = jest.fn();
|
||
const initialEnv = {
|
||
FIRST_KEY: "first_value",
|
||
SECOND_KEY: "second_value",
|
||
THIRD_KEY: "third_value",
|
||
};
|
||
renderSidebar({ env: initialEnv, setEnv });
|
||
|
||
openEnvVarsSection();
|
||
|
||
const firstKeyInput = screen.getByDisplayValue("FIRST_KEY");
|
||
fireEvent.change(firstKeyInput, { target: { value: "NEW_FIRST_KEY" } });
|
||
|
||
expect(setEnv).toHaveBeenCalledWith({
|
||
NEW_FIRST_KEY: "first_value",
|
||
SECOND_KEY: "second_value",
|
||
THIRD_KEY: "third_value",
|
||
});
|
||
});
|
||
|
||
it("should maintain order when editing middle key", () => {
|
||
const setEnv = jest.fn();
|
||
const initialEnv = {
|
||
FIRST_KEY: "first_value",
|
||
SECOND_KEY: "second_value",
|
||
THIRD_KEY: "third_value",
|
||
};
|
||
renderSidebar({ env: initialEnv, setEnv });
|
||
|
||
openEnvVarsSection();
|
||
|
||
const middleKeyInput = screen.getByDisplayValue("SECOND_KEY");
|
||
fireEvent.change(middleKeyInput, { target: { value: "NEW_SECOND_KEY" } });
|
||
|
||
expect(setEnv).toHaveBeenCalledWith({
|
||
FIRST_KEY: "first_value",
|
||
NEW_SECOND_KEY: "second_value",
|
||
THIRD_KEY: "third_value",
|
||
});
|
||
});
|
||
|
||
it("should maintain order when editing last key", () => {
|
||
const setEnv = jest.fn();
|
||
const initialEnv = {
|
||
FIRST_KEY: "first_value",
|
||
SECOND_KEY: "second_value",
|
||
THIRD_KEY: "third_value",
|
||
};
|
||
renderSidebar({ env: initialEnv, setEnv });
|
||
|
||
openEnvVarsSection();
|
||
|
||
const lastKeyInput = screen.getByDisplayValue("THIRD_KEY");
|
||
fireEvent.change(lastKeyInput, { target: { value: "NEW_THIRD_KEY" } });
|
||
|
||
expect(setEnv).toHaveBeenCalledWith({
|
||
FIRST_KEY: "first_value",
|
||
SECOND_KEY: "second_value",
|
||
NEW_THIRD_KEY: "third_value",
|
||
});
|
||
});
|
||
|
||
it("should maintain order during key editing", () => {
|
||
const setEnv = jest.fn();
|
||
const initialEnv = {
|
||
KEY1: "value1",
|
||
KEY2: "value2",
|
||
};
|
||
renderSidebar({ env: initialEnv, setEnv });
|
||
|
||
openEnvVarsSection();
|
||
|
||
// Type "NEW_" one character at a time
|
||
const key1Input = screen.getByDisplayValue("KEY1");
|
||
"NEW_".split("").forEach((char) => {
|
||
fireEvent.change(key1Input, {
|
||
target: { value: char + "KEY1".slice(1) },
|
||
});
|
||
});
|
||
|
||
// Verify the last setEnv call maintains the order
|
||
const lastCall = setEnv.mock.calls[
|
||
setEnv.mock.calls.length - 1
|
||
][0] as Record<string, string>;
|
||
const entries = Object.entries(lastCall);
|
||
|
||
// The values should stay with their original keys
|
||
expect(entries[0][1]).toBe("value1"); // First entry should still have value1
|
||
expect(entries[1][1]).toBe("value2"); // Second entry should still have value2
|
||
});
|
||
});
|
||
|
||
describe("Multiple Operations", () => {
|
||
it("should maintain state after multiple key edits", () => {
|
||
const setEnv = jest.fn();
|
||
const initialEnv = {
|
||
FIRST_KEY: "first_value",
|
||
SECOND_KEY: "second_value",
|
||
};
|
||
const { rerender } = renderSidebar({ env: initialEnv, setEnv });
|
||
|
||
openEnvVarsSection();
|
||
|
||
// First key edit
|
||
const firstKeyInput = screen.getByDisplayValue("FIRST_KEY");
|
||
fireEvent.change(firstKeyInput, { target: { value: "NEW_FIRST_KEY" } });
|
||
|
||
// Get the updated env from the first setEnv call
|
||
const updatedEnv = setEnv.mock.calls[0][0] as Record<string, string>;
|
||
|
||
// Rerender with the updated env
|
||
rerender(
|
||
<TooltipProvider>
|
||
<Sidebar {...defaultProps} env={updatedEnv} setEnv={setEnv} />
|
||
</TooltipProvider>,
|
||
);
|
||
|
||
// Second key edit
|
||
const secondKeyInput = screen.getByDisplayValue("SECOND_KEY");
|
||
fireEvent.change(secondKeyInput, { target: { value: "NEW_SECOND_KEY" } });
|
||
|
||
// Verify the final state matches what we expect
|
||
expect(setEnv).toHaveBeenLastCalledWith({
|
||
NEW_FIRST_KEY: "first_value",
|
||
NEW_SECOND_KEY: "second_value",
|
||
});
|
||
});
|
||
|
||
it("should maintain visibility state after key edit", () => {
|
||
const initialEnv = { TEST_KEY: "test_value" };
|
||
const { rerender } = renderSidebar({ env: initialEnv });
|
||
|
||
openEnvVarsSection();
|
||
|
||
// Show the value
|
||
const toggleButton = screen.getByRole("button", { name: /show value/i });
|
||
fireEvent.click(toggleButton);
|
||
|
||
const valueInput = screen.getByDisplayValue("test_value");
|
||
expect(valueInput).toHaveProperty("type", "text");
|
||
|
||
// Edit the key
|
||
const keyInput = screen.getByDisplayValue("TEST_KEY");
|
||
fireEvent.change(keyInput, { target: { value: "NEW_KEY" } });
|
||
|
||
// Rerender with updated env
|
||
rerender(
|
||
<TooltipProvider>
|
||
<Sidebar {...defaultProps} env={{ NEW_KEY: "test_value" }} />
|
||
</TooltipProvider>,
|
||
);
|
||
|
||
// Value should still be visible
|
||
const updatedValueInput = screen.getByDisplayValue("test_value");
|
||
expect(updatedValueInput).toHaveProperty("type", "text");
|
||
});
|
||
});
|
||
|
||
describe("Edge Cases", () => {
|
||
it("should handle empty key", () => {
|
||
const setEnv = jest.fn();
|
||
const initialEnv = { TEST_KEY: "test_value" };
|
||
renderSidebar({ env: initialEnv, setEnv });
|
||
|
||
openEnvVarsSection();
|
||
|
||
const keyInput = screen.getByDisplayValue("TEST_KEY");
|
||
fireEvent.change(keyInput, { target: { value: "" } });
|
||
|
||
expect(setEnv).toHaveBeenCalledWith({ "": "test_value" });
|
||
});
|
||
|
||
it("should handle special characters in key", () => {
|
||
const setEnv = jest.fn();
|
||
const initialEnv = { TEST_KEY: "test_value" };
|
||
renderSidebar({ env: initialEnv, setEnv });
|
||
|
||
openEnvVarsSection();
|
||
|
||
const keyInput = screen.getByDisplayValue("TEST_KEY");
|
||
fireEvent.change(keyInput, { target: { value: "TEST-KEY@123" } });
|
||
|
||
expect(setEnv).toHaveBeenCalledWith({ "TEST-KEY@123": "test_value" });
|
||
});
|
||
|
||
it("should handle unicode characters", () => {
|
||
const setEnv = jest.fn();
|
||
const initialEnv = { TEST_KEY: "test_value" };
|
||
renderSidebar({ env: initialEnv, setEnv });
|
||
|
||
openEnvVarsSection();
|
||
|
||
const keyInput = screen.getByDisplayValue("TEST_KEY");
|
||
fireEvent.change(keyInput, { target: { value: "TEST_🔑" } });
|
||
|
||
expect(setEnv).toHaveBeenCalledWith({ "TEST_🔑": "test_value" });
|
||
});
|
||
|
||
it("should handle very long key names", () => {
|
||
const setEnv = jest.fn();
|
||
const initialEnv = { TEST_KEY: "test_value" };
|
||
renderSidebar({ env: initialEnv, setEnv });
|
||
|
||
openEnvVarsSection();
|
||
|
||
const keyInput = screen.getByDisplayValue("TEST_KEY");
|
||
const longKey = "A".repeat(100);
|
||
fireEvent.change(keyInput, { target: { value: longKey } });
|
||
|
||
expect(setEnv).toHaveBeenCalledWith({ [longKey]: "test_value" });
|
||
});
|
||
});
|
||
|
||
describe("Configuration Operations", () => {
|
||
const openConfigSection = () => {
|
||
const button = screen.getByTestId("config-button");
|
||
fireEvent.click(button);
|
||
};
|
||
|
||
it("should update MCP server request timeout", () => {
|
||
const setConfig = jest.fn();
|
||
renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
|
||
|
||
openConfigSection();
|
||
|
||
const timeoutInput = screen.getByTestId(
|
||
"MCP_SERVER_REQUEST_TIMEOUT-input",
|
||
);
|
||
fireEvent.change(timeoutInput, { target: { value: "5000" } });
|
||
|
||
expect(setConfig).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
MCP_SERVER_REQUEST_TIMEOUT: {
|
||
label: "Request Timeout",
|
||
description: "Timeout for requests to the MCP server (ms)",
|
||
value: 5000,
|
||
},
|
||
}),
|
||
);
|
||
});
|
||
|
||
it("should update MCP server proxy address", () => {
|
||
const setConfig = jest.fn();
|
||
renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
|
||
|
||
openConfigSection();
|
||
|
||
const proxyAddressInput = screen.getByTestId(
|
||
"MCP_PROXY_FULL_ADDRESS-input",
|
||
);
|
||
fireEvent.change(proxyAddressInput, {
|
||
target: { value: "http://localhost:8080" },
|
||
});
|
||
|
||
expect(setConfig).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
MCP_PROXY_FULL_ADDRESS: {
|
||
label: "Inspector Proxy Address",
|
||
description:
|
||
"Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577",
|
||
value: "http://localhost:8080",
|
||
},
|
||
}),
|
||
);
|
||
});
|
||
|
||
it("should update max total timeout", () => {
|
||
const setConfig = jest.fn();
|
||
renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
|
||
|
||
openConfigSection();
|
||
|
||
const maxTotalTimeoutInput = screen.getByTestId(
|
||
"MCP_REQUEST_MAX_TOTAL_TIMEOUT-input",
|
||
);
|
||
fireEvent.change(maxTotalTimeoutInput, {
|
||
target: { value: "10000" },
|
||
});
|
||
|
||
expect(setConfig).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
MCP_REQUEST_MAX_TOTAL_TIMEOUT: {
|
||
label: "Maximum Total Timeout",
|
||
description:
|
||
"Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications)",
|
||
value: 10000,
|
||
},
|
||
}),
|
||
);
|
||
});
|
||
|
||
it("should handle invalid timeout values entered by user", () => {
|
||
const setConfig = jest.fn();
|
||
renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
|
||
|
||
openConfigSection();
|
||
|
||
const timeoutInput = screen.getByTestId(
|
||
"MCP_SERVER_REQUEST_TIMEOUT-input",
|
||
);
|
||
fireEvent.change(timeoutInput, { target: { value: "abc1" } });
|
||
|
||
expect(setConfig).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
MCP_SERVER_REQUEST_TIMEOUT: {
|
||
label: "Request Timeout",
|
||
description: "Timeout for requests to the MCP server (ms)",
|
||
value: 0,
|
||
},
|
||
}),
|
||
);
|
||
});
|
||
|
||
it("should maintain configuration state after multiple updates", () => {
|
||
const setConfig = jest.fn();
|
||
const { rerender } = renderSidebar({
|
||
config: DEFAULT_INSPECTOR_CONFIG,
|
||
setConfig,
|
||
});
|
||
|
||
openConfigSection();
|
||
// First update
|
||
const timeoutInput = screen.getByTestId(
|
||
"MCP_SERVER_REQUEST_TIMEOUT-input",
|
||
);
|
||
fireEvent.change(timeoutInput, { target: { value: "5000" } });
|
||
|
||
// Get the updated config from the first setConfig call
|
||
const updatedConfig = setConfig.mock.calls[0][0] as InspectorConfig;
|
||
|
||
// Rerender with the updated config
|
||
rerender(
|
||
<TooltipProvider>
|
||
<Sidebar
|
||
{...defaultProps}
|
||
config={updatedConfig}
|
||
setConfig={setConfig}
|
||
/>
|
||
</TooltipProvider>,
|
||
);
|
||
|
||
// Second update
|
||
const updatedTimeoutInput = screen.getByTestId(
|
||
"MCP_SERVER_REQUEST_TIMEOUT-input",
|
||
);
|
||
fireEvent.change(updatedTimeoutInput, { target: { value: "3000" } });
|
||
|
||
// Verify the final state matches what we expect
|
||
expect(setConfig).toHaveBeenLastCalledWith(
|
||
expect.objectContaining({
|
||
MCP_SERVER_REQUEST_TIMEOUT: {
|
||
label: "Request Timeout",
|
||
description: "Timeout for requests to the MCP server (ms)",
|
||
value: 3000,
|
||
},
|
||
}),
|
||
);
|
||
});
|
||
});
|
||
});
|