Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
"react-intl": "^10.1.1",
"shadcn": "^4.2.0",
"tailwind-merge": "^3.5.0",
"tw-animate-css": "^1.4.0"
"tw-animate-css": "^1.4.0",
"zod": "^3.25.76"
},
"devDependencies": {
"@formatjs/cli": "^6.14.1",
Expand Down
105 changes: 62 additions & 43 deletions client/src/components/mcp-servers/MCPServerForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,38 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MCPServerForm } from "./MCPServerForm";

// Mock the router
const mockNavigate = vi.fn();
vi.mock("@/router", () => ({
useRouter: () => ({
navigate: mockNavigate,
}),
}));
import { RouterProvider } from "@/router";

describe("MCPServerForm", () => {
const defaultProps = {
isOpen: true,
onToggle: vi.fn(),
};

// Helper to render with router
const renderWithRouter = (ui: React.ReactElement) => {
return render(<RouterProvider>{ui}</RouterProvider>);
};

beforeEach(() => {
vi.clearAllMocks();
// Set initial path for router
window.history.pushState({}, "", "/app/servers");
});

describe("Rendering", () => {
it("should not render when isOpen is false", () => {
render(<MCPServerForm isOpen={false} onToggle={defaultProps.onToggle} />);
renderWithRouter(<MCPServerForm isOpen={false} onToggle={defaultProps.onToggle} />);
expect(screen.queryByText("Connect MCP server")).not.toBeInTheDocument();
});

it("should render when isOpen is true", () => {
render(<MCPServerForm {...defaultProps} />);
renderWithRouter(<MCPServerForm {...defaultProps} />);
expect(screen.getByText("Connect MCP server")).toBeInTheDocument();
});

it("should render all required form fields", () => {
render(<MCPServerForm {...defaultProps} />);
renderWithRouter(<MCPServerForm {...defaultProps} />);

expect(screen.getByLabelText("Streamable HTTP")).toBeInTheDocument();
expect(screen.getByLabelText("SSE")).toBeInTheDocument();
Expand All @@ -50,7 +50,7 @@ describe("MCPServerForm", () => {
});

it("should render link to server catalog", () => {
render(<MCPServerForm {...defaultProps} />);
renderWithRouter(<MCPServerForm {...defaultProps} />);

const catalogLink = screen.getByRole("button", { name: /mcp server catalog/i });
expect(catalogLink).toBeInTheDocument();
Expand All @@ -59,15 +59,15 @@ describe("MCPServerForm", () => {

describe("Transport Type Selection", () => {
it("should have Streamable HTTP selected by default", () => {
render(<MCPServerForm {...defaultProps} />);
renderWithRouter(<MCPServerForm {...defaultProps} />);

const streamableHttpRadio = screen.getByRole("radio", { name: /Streamable HTTP/i });
expect(streamableHttpRadio).toBeChecked();
});

it("should allow switching to SSE transport", async () => {
const user = userEvent.setup();
render(<MCPServerForm {...defaultProps} />);
renderWithRouter(<MCPServerForm {...defaultProps} />);

const sseRadio = screen.getByRole("radio", { name: /SSE/i });
await user.click(sseRadio);
Expand All @@ -79,7 +79,7 @@ describe("MCPServerForm", () => {
describe("Form Input Handling", () => {
it("should update name field when typing", async () => {
const user = userEvent.setup();
render(<MCPServerForm {...defaultProps} />);
renderWithRouter(<MCPServerForm {...defaultProps} />);

const nameInput = screen.getByLabelText(/Name/i);
await user.type(nameInput, "Test Server");
Expand All @@ -89,7 +89,7 @@ describe("MCPServerForm", () => {

it("should update URL field when typing", async () => {
const user = userEvent.setup();
render(<MCPServerForm {...defaultProps} />);
renderWithRouter(<MCPServerForm {...defaultProps} />);

const urlInput = screen.getByLabelText(/URL/i);
await user.type(urlInput, "http://localhost:3000");
Expand All @@ -99,7 +99,7 @@ describe("MCPServerForm", () => {

it("should update description field when typing", async () => {
const user = userEvent.setup();
render(<MCPServerForm {...defaultProps} />);
renderWithRouter(<MCPServerForm {...defaultProps} />);

const descriptionInput = screen.getByPlaceholderText(/Add an optional description/i);
await user.type(descriptionInput, "Test description");
Expand All @@ -110,15 +110,15 @@ describe("MCPServerForm", () => {

describe("Advanced Settings", () => {
it("should not show advanced settings by default", () => {
render(<MCPServerForm {...defaultProps} />);
renderWithRouter(<MCPServerForm {...defaultProps} />);

expect(screen.queryByText("Visibility")).not.toBeInTheDocument();
expect(screen.queryByText("Authentication type")).not.toBeInTheDocument();
});

it("should toggle advanced settings when button is clicked", async () => {
const user = userEvent.setup();
render(<MCPServerForm {...defaultProps} />);
renderWithRouter(<MCPServerForm {...defaultProps} />);

const advancedButton = screen.getByRole("button", { name: /Advanced settings/i });
await user.click(advancedButton);
Expand All @@ -129,7 +129,7 @@ describe("MCPServerForm", () => {

it("should hide advanced settings when toggled again", async () => {
const user = userEvent.setup();
render(<MCPServerForm {...defaultProps} />);
renderWithRouter(<MCPServerForm {...defaultProps} />);

const advancedButton = screen.getByRole("button", { name: /Advanced settings/i });

Expand All @@ -146,7 +146,7 @@ describe("MCPServerForm", () => {

it("should render AdvancedSettings component when expanded", async () => {
const user = userEvent.setup();
render(<MCPServerForm {...defaultProps} />);
renderWithRouter(<MCPServerForm {...defaultProps} />);

const advancedButton = screen.getByRole("button", { name: /Advanced settings/i });
await user.click(advancedButton);
Expand All @@ -163,7 +163,7 @@ describe("MCPServerForm", () => {
describe("Authentication Type Selection", () => {
beforeEach(async () => {
const user = userEvent.setup();
render(<MCPServerForm {...defaultProps} />);
renderWithRouter(<MCPServerForm {...defaultProps} />);

// Open advanced settings
const advancedButton = screen.getByRole("button", { name: /Advanced settings/i });
Expand Down Expand Up @@ -342,17 +342,25 @@ describe("MCPServerForm", () => {
it("should call onToggle when form is submitted", async () => {
const user = userEvent.setup();
const onToggle = vi.fn();
render(<MCPServerForm isOpen={true} onToggle={onToggle} />);
renderWithRouter(<MCPServerForm isOpen={true} onToggle={onToggle} />);

// Fill in required fields
const nameInput = screen.getByLabelText(/Name/i);
const urlInput = screen.getByLabelText(/URL/i);
await user.type(nameInput, "Test Server");
await user.type(urlInput, "http://localhost:3000");

const submitButton = screen.getByRole("button", { name: /Connect server/i });
await user.click(submitButton);

expect(onToggle).toHaveBeenCalledTimes(1);
await waitFor(() => {
expect(onToggle).toHaveBeenCalledTimes(1);
});
});

it("should reset form fields after submission", async () => {
const user = userEvent.setup();
render(<MCPServerForm {...defaultProps} />);
renderWithRouter(<MCPServerForm {...defaultProps} />);

// Fill in form
const nameInput = screen.getByLabelText(/Name/i);
Expand All @@ -370,7 +378,13 @@ describe("MCPServerForm", () => {

it("should prevent default form submission", async () => {
const user = userEvent.setup();
render(<MCPServerForm {...defaultProps} />);
renderWithRouter(<MCPServerForm {...defaultProps} />);

// Fill in required fields
const nameInput = screen.getByLabelText(/Name/i);
const urlInput = screen.getByLabelText(/URL/i);
await user.type(nameInput, "Test Server");
await user.type(urlInput, "http://localhost:3000");

const form = screen.getByRole("button", { name: /Connect server/i }).closest("form");
const submitHandler = vi.fn((e) => e.preventDefault());
Expand All @@ -380,7 +394,9 @@ describe("MCPServerForm", () => {
const submitButton = screen.getByRole("button", { name: /Connect server/i });
await user.click(submitButton);

expect(submitHandler).toHaveBeenCalled();
await waitFor(() => {
expect(submitHandler).toHaveBeenCalled();
});
}
});
});
Expand All @@ -389,7 +405,7 @@ describe("MCPServerForm", () => {
it("should call onToggle when cancel button is clicked", async () => {
const user = userEvent.setup();
const onToggle = vi.fn();
render(<MCPServerForm isOpen={true} onToggle={onToggle} />);
renderWithRouter(<MCPServerForm isOpen={true} onToggle={onToggle} />);

const cancelButton = screen.getByRole("button", { name: /Cancel/i });
await user.click(cancelButton);
Expand All @@ -402,26 +418,29 @@ describe("MCPServerForm", () => {
it("should navigate to server catalog when link is clicked", async () => {
const user = userEvent.setup();
const onToggle = vi.fn();
render(<MCPServerForm isOpen={true} onToggle={onToggle} />);
renderWithRouter(<MCPServerForm isOpen={true} onToggle={onToggle} />);

const catalogLink = screen.getByRole("button", { name: /mcp server catalog/i });
await user.click(catalogLink);

expect(onToggle).toHaveBeenCalledTimes(1);
expect(mockNavigate).toHaveBeenCalledWith("/app/server-catalog");
// Verify navigation by checking window location
await waitFor(() => {
expect(window.location.pathname).toBe("/app/server-catalog");
});
});
});

describe("Accessibility", () => {
it("should have proper ARIA labels for transport type radio group", () => {
render(<MCPServerForm {...defaultProps} />);
renderWithRouter(<MCPServerForm {...defaultProps} />);

const radioGroup = screen.getByRole("radiogroup", { name: /Server transport type/i });
expect(radioGroup).toBeInTheDocument();
});

it("should have required indicators on required fields", () => {
render(<MCPServerForm {...defaultProps} />);
renderWithRouter(<MCPServerForm {...defaultProps} />);

const nameLabel = screen.getByText(/Name/i).closest("label");
const urlLabel = screen.getByText(/URL/i).closest("label");
Expand All @@ -431,15 +450,15 @@ describe("MCPServerForm", () => {
});

it("should have screen reader text for required fields", () => {
render(<MCPServerForm {...defaultProps} />);
renderWithRouter(<MCPServerForm {...defaultProps} />);

const srTexts = screen.getAllByText("(required)");
expect(srTexts.length).toBeGreaterThan(0);
});

it("should have proper aria-expanded attribute on advanced settings button", async () => {
const user = userEvent.setup();
render(<MCPServerForm {...defaultProps} />);
renderWithRouter(<MCPServerForm {...defaultProps} />);

const advancedButton = screen.getByRole("button", { name: /Advanced settings/i });

Expand All @@ -454,7 +473,7 @@ describe("MCPServerForm", () => {
describe("Visual Feedback", () => {
it("should rotate chevron icon when advanced settings are expanded", async () => {
const user = userEvent.setup();
render(<MCPServerForm {...defaultProps} />);
renderWithRouter(<MCPServerForm {...defaultProps} />);

const advancedButton = screen.getByRole("button", { name: /Advanced settings/i });
const chevron = advancedButton.querySelector("svg");
Expand All @@ -470,7 +489,7 @@ describe("MCPServerForm", () => {
describe("Integration with Child Components", () => {
it("should pass correct props to AdvancedSettings when expanded", async () => {
const user = userEvent.setup();
render(<MCPServerForm {...defaultProps} />);
renderWithRouter(<MCPServerForm {...defaultProps} />);

const advancedButton = screen.getByRole("button", { name: /Advanced settings/i });
await user.click(advancedButton);
Expand All @@ -484,7 +503,7 @@ describe("MCPServerForm", () => {
describe("CA Certificate Upload", () => {
it("should render CA certificate upload section in advanced settings", async () => {
const user = userEvent.setup();
render(<MCPServerForm {...defaultProps} />);
renderWithRouter(<MCPServerForm {...defaultProps} />);

const advancedButton = screen.getByRole("button", { name: /Advanced settings/i });
await user.click(advancedButton);
Expand All @@ -495,7 +514,7 @@ describe("MCPServerForm", () => {

it("should show file type information for CA certificate", async () => {
const user = userEvent.setup();
render(<MCPServerForm {...defaultProps} />);
renderWithRouter(<MCPServerForm {...defaultProps} />);

const advancedButton = screen.getByRole("button", { name: /Advanced settings/i });
await user.click(advancedButton);
Expand All @@ -507,7 +526,7 @@ describe("MCPServerForm", () => {

it("should allow clicking upload button to trigger file selection", async () => {
const user = userEvent.setup();
render(<MCPServerForm {...defaultProps} />);
renderWithRouter(<MCPServerForm {...defaultProps} />);

const advancedButton = screen.getByRole("button", { name: /Advanced settings/i });
await user.click(advancedButton);
Expand All @@ -521,7 +540,7 @@ describe("MCPServerForm", () => {

it("should handle file upload through drag and drop area", async () => {
const user = userEvent.setup();
render(<MCPServerForm {...defaultProps} />);
renderWithRouter(<MCPServerForm {...defaultProps} />);

const advancedButton = screen.getByRole("button", { name: /Advanced settings/i });
await user.click(advancedButton);
Expand All @@ -535,7 +554,7 @@ describe("MCPServerForm", () => {

it("should accept valid certificate file extensions", async () => {
const user = userEvent.setup();
render(<MCPServerForm {...defaultProps} />);
renderWithRouter(<MCPServerForm {...defaultProps} />);

const advancedButton = screen.getByRole("button", { name: /Advanced settings/i });
await user.click(advancedButton);
Expand All @@ -558,7 +577,7 @@ describe("MCPServerForm", () => {

it("should handle multiple certificate files", async () => {
const user = userEvent.setup();
render(<MCPServerForm {...defaultProps} />);
renderWithRouter(<MCPServerForm {...defaultProps} />);

const advancedButton = screen.getByRole("button", { name: /Advanced settings/i });
await user.click(advancedButton);
Expand Down Expand Up @@ -591,7 +610,7 @@ describe("MCPServerForm", () => {
it("should log selected files to console", async () => {
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
const user = userEvent.setup();
render(<MCPServerForm {...defaultProps} />);
renderWithRouter(<MCPServerForm {...defaultProps} />);

const advancedButton = screen.getByRole("button", { name: /Advanced settings/i });
await user.click(advancedButton);
Expand Down
Loading
Loading