import { render, screen, fireEvent, within } from "@testing-library/react"; import { useState } from "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, data: "First notification", }, }, { method: "notifications/progress", params: { progressToken: "test-token", progress: 50, total: 100, }, }, ]; 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(); }); it("clears request history when Clear is clicked", () => { const Wrapper = () => { const [history, setHistory] = useState(mockRequestHistory); return ( setHistory([])} /> ); }; render(); // Verify items are present initially expect(screen.getByText("2. test/method2")).toBeTruthy(); expect(screen.getByText("1. test/method1")).toBeTruthy(); // Click Clear in History header (scoped by the History heading's container) const historyHeader = screen.getByText("History"); const historyHeaderContainer = historyHeader.parentElement as HTMLElement; const historyClearButton = within(historyHeaderContainer).getByRole( "button", { name: "Clear" }, ); fireEvent.click(historyClearButton); // History should now be empty expect(screen.getByText("No history yet")).toBeTruthy(); }); it("clears server notifications when Clear is clicked", () => { const Wrapper = () => { const [notifications, setNotifications] = useState(mockNotifications); return ( setNotifications([])} /> ); }; render(); // Verify items are present initially expect(screen.getByText("2. notifications/progress")).toBeTruthy(); expect(screen.getByText("1. notifications/message")).toBeTruthy(); // Click Clear in Server Notifications header (scoped by its heading's container) const notifHeader = screen.getByText("Server Notifications"); const notifHeaderContainer = notifHeader.parentElement as HTMLElement; const notifClearButton = within(notifHeaderContainer).getByRole("button", { name: "Clear", }); fireEvent.click(notifClearButton); // Notifications should now be empty expect(screen.getByText("No notifications yet")).toBeTruthy(); }); });