diff --git a/client/src/App.tsx b/client/src/App.tsx index 26eb44c5..3ceafcae 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -52,7 +52,7 @@ import { z } from "zod"; import "./App.css"; import AuthDebugger from "./components/AuthDebugger"; import ConsoleTab from "./components/ConsoleTab"; -import HistoryAndNotifications from "./components/History"; +import HistoryAndNotifications from "./components/HistoryAndNotifications"; import PingTab from "./components/PingTab"; import PromptsTab, { Prompt } from "./components/PromptsTab"; import ResourcesTab from "./components/ResourcesTab"; diff --git a/client/src/components/History.tsx b/client/src/components/HistoryAndNotifications.tsx similarity index 89% rename from client/src/components/History.tsx rename to client/src/components/HistoryAndNotifications.tsx index 78394dea..34c035f5 100644 --- a/client/src/components/History.tsx +++ b/client/src/components/HistoryAndNotifications.tsx @@ -110,15 +110,27 @@ const HistoryAndNotifications = ({ >
toggleNotificationExpansion(index)} + onClick={() => + toggleNotificationExpansion( + serverNotifications.length - 1 - index, + ) + } > {serverNotifications.length - index}.{" "} {notification.method} - {expandedNotifications[index] ? "▼" : "▶"} + + {expandedNotifications[ + serverNotifications.length - 1 - index + ] + ? "▼" + : "▶"} +
- {expandedNotifications[index] && ( + {expandedNotifications[ + serverNotifications.length - 1 - index + ] && (
diff --git a/client/src/components/__tests__/HistoryAndNotifications.test.tsx b/client/src/components/__tests__/HistoryAndNotifications.test.tsx new file mode 100644 index 00000000..42c58519 --- /dev/null +++ b/client/src/components/__tests__/HistoryAndNotifications.test.tsx @@ -0,0 +1,226 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, it, expect, jest } from "@jest/globals"; +import HistoryAndNotifications from "../HistoryAndNotifications"; +import { ServerNotification } from "@modelcontextprotocol/sdk/types.js"; + +// Mock JsonView component +jest.mock("../JsonView", () => { + return function JsonView({ data }: { data: string }) { + return
{data}
; + }; +}); + +describe("HistoryAndNotifications", () => { + const mockRequestHistory = [ + { + request: JSON.stringify({ method: "test/method1", params: {} }), + response: JSON.stringify({ result: "success" }), + }, + { + request: JSON.stringify({ method: "test/method2", params: {} }), + response: JSON.stringify({ result: "success" }), + }, + ]; + + const mockNotifications: ServerNotification[] = [ + { + method: "notifications/message", + params: { + level: "info" as const, + message: "First notification", + }, + }, + { + method: "notifications/progress", + params: { + progressToken: "test-token", + progress: 50, + message: "Second notification", + }, + }, + ]; + + it("renders history and notifications sections", () => { + render( + , + ); + + expect(screen.getByText("History")).toBeTruthy(); + expect(screen.getByText("Server Notifications")).toBeTruthy(); + }); + + it("displays request history items with correct numbering", () => { + render( + , + ); + + // Items should be numbered in reverse order (newest first) + expect(screen.getByText("2. test/method2")).toBeTruthy(); + expect(screen.getByText("1. test/method1")).toBeTruthy(); + }); + + it("displays server notifications with correct numbering", () => { + render( + , + ); + + // Items should be numbered in reverse order (newest first) + expect(screen.getByText("2. notifications/progress")).toBeTruthy(); + expect(screen.getByText("1. notifications/message")).toBeTruthy(); + }); + + it("expands and collapses request items when clicked", () => { + render( + , + ); + + const firstRequestHeader = screen.getByText("2. test/method2"); + + // Initially collapsed - should show ▶ arrows (there are multiple) + expect(screen.getAllByText("▶")).toHaveLength(2); + expect(screen.queryByText("Request:")).toBeNull(); + + // Click to expand + fireEvent.click(firstRequestHeader); + + // Should now be expanded - one ▼ and one ▶ + expect(screen.getByText("▼")).toBeTruthy(); + expect(screen.getAllByText("▶")).toHaveLength(1); + expect(screen.getByText("Request:")).toBeTruthy(); + expect(screen.getByText("Response:")).toBeTruthy(); + }); + + it("expands and collapses notification items when clicked", () => { + render( + , + ); + + const firstNotificationHeader = screen.getByText( + "2. notifications/progress", + ); + + // Initially collapsed + expect(screen.getAllByText("▶")).toHaveLength(2); + expect(screen.queryByText("Details:")).toBeNull(); + + // Click to expand + fireEvent.click(firstNotificationHeader); + + // Should now be expanded + expect(screen.getByText("▼")).toBeTruthy(); + expect(screen.getAllByText("▶")).toHaveLength(1); + expect(screen.getByText("Details:")).toBeTruthy(); + }); + + it("maintains expanded state when new notifications are added", () => { + const { rerender } = render( + , + ); + + // Find and expand the older notification (should be "1. notifications/message") + const olderNotificationHeader = screen.getByText( + "1. notifications/message", + ); + fireEvent.click(olderNotificationHeader); + + // Verify it's expanded + expect(screen.getByText("Details:")).toBeTruthy(); + + // Add a new notification at the beginning (simulating real behavior) + const newNotifications: ServerNotification[] = [ + { + method: "notifications/resources/updated", + params: { uri: "file://test.txt" }, + }, + ...mockNotifications, + ]; + + // Re-render with new notifications + rerender( + , + ); + + // The original notification should still be expanded + // It's now numbered as "2. notifications/message" due to the new item + expect(screen.getByText("3. notifications/progress")).toBeTruthy(); + expect(screen.getByText("2. notifications/message")).toBeTruthy(); + expect(screen.getByText("1. notifications/resources/updated")).toBeTruthy(); + + // The originally expanded notification should still show its details + expect(screen.getByText("Details:")).toBeTruthy(); + }); + + it("maintains expanded state when new requests are added", () => { + const { rerender } = render( + , + ); + + // Find and expand the older request (should be "1. test/method1") + const olderRequestHeader = screen.getByText("1. test/method1"); + fireEvent.click(olderRequestHeader); + + // Verify it's expanded + expect(screen.getByText("Request:")).toBeTruthy(); + expect(screen.getByText("Response:")).toBeTruthy(); + + // Add a new request at the beginning + const newRequestHistory = [ + { + request: JSON.stringify({ method: "test/new_method", params: {} }), + response: JSON.stringify({ result: "new success" }), + }, + ...mockRequestHistory, + ]; + + // Re-render with new request history + rerender( + , + ); + + // The original request should still be expanded + // It's now numbered as "2. test/method1" due to the new item + expect(screen.getByText("3. test/method2")).toBeTruthy(); + expect(screen.getByText("2. test/method1")).toBeTruthy(); + expect(screen.getByText("1. test/new_method")).toBeTruthy(); + + // The originally expanded request should still show its details + expect(screen.getByText("Request:")).toBeTruthy(); + expect(screen.getByText("Response:")).toBeTruthy(); + }); + + it("displays empty state messages when no data is available", () => { + render( + , + ); + + expect(screen.getByText("No history yet")).toBeTruthy(); + expect(screen.getByText("No notifications yet")).toBeTruthy(); + }); +});