inspector/clients/web/src/components/screens/ProtocolScreen/ProtocolScreen.test.tsx
cliffhall 9824055425 refactor(web): unify message-direction arrows and rename Pinned section (#1623)
- Direction labels now both point right so the arrow reads clearly at
  badge size: incoming "client ← server" becomes "server → client"
  (from → to), in MessageDirectionBadge and MessageDirectionFilter
  (shared by the Protocol and Network views).
- The Protocol list's pinned section header now reads "Pinned Messages"
  (was "Pinned Requests"), matching the "Messages" panel title.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
2026-07-09 19:45:52 -04:00

166 lines
5.4 KiB
TypeScript

import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import type { MessageEntry } from "@inspector/core/mcp/types.js";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { ProtocolScreen } from "./ProtocolScreen";
import { EMPTY_PROTOCOL_UI } from "../screenUiState";
const sampleEntries: MessageEntry[] = [
{
id: "1",
timestamp: new Date(),
direction: "request",
message: { jsonrpc: "2.0", id: 1, method: "tools/list" },
},
{
id: "2",
timestamp: new Date(),
direction: "response",
message: { jsonrpc: "2.0", id: 1, result: {} },
},
];
const baseProps = {
entries: sampleEntries,
pinnedIds: new Set<string>(),
ui: EMPTY_PROTOCOL_UI,
onUiChange: vi.fn(),
onClearAll: vi.fn(),
onExport: vi.fn(),
onClearSection: vi.fn(),
onExportSection: vi.fn(),
onReplay: vi.fn(),
onTogglePin: vi.fn(),
sortDirection: "newest-first" as const,
onSortChange: vi.fn(),
compact: true,
onToggleCompact: vi.fn(),
};
describe("ProtocolScreen", () => {
it("renders the controls and panel", () => {
renderWithMantine(<ProtocolScreen {...baseProps} />);
expect(screen.getByText("Protocol")).toBeInTheDocument();
expect(screen.getByPlaceholderText("Search...")).toBeInTheDocument();
});
it("renders empty state when there are no entries", () => {
renderWithMantine(<ProtocolScreen {...baseProps} entries={[]} />);
expect(screen.getByText("Protocol")).toBeInTheDocument();
});
it("invokes onClearAll when clear is triggered", async () => {
const user = userEvent.setup();
const onClearAll = vi.fn();
renderWithMantine(
<ProtocolScreen {...baseProps} onClearAll={onClearAll} />,
);
const clearButton = screen.getByRole("button", { name: /Clear/ });
await user.click(clearButton);
expect(onClearAll).toHaveBeenCalled();
});
it("emits the search text through onUiChange", async () => {
const user = userEvent.setup();
const onUiChange = vi.fn();
renderWithMantine(
<ProtocolScreen {...baseProps} onUiChange={onUiChange} />,
);
await user.type(screen.getByPlaceholderText("Search..."), "t");
expect(onUiChange).toHaveBeenCalledWith(
expect.objectContaining({ search: "t" }),
);
});
it("toggles a single message direction through onUiChange", async () => {
const user = userEvent.setup();
const onUiChange = vi.fn();
renderWithMantine(
<ProtocolScreen {...baseProps} onUiChange={onUiChange} />,
);
await user.click(screen.getByRole("button", { name: "server → client" }));
expect(onUiChange).toHaveBeenCalledWith(
expect.objectContaining({
visibleDirections: { client: true, server: false },
}),
);
});
it("toggles all message directions off through onUiChange", async () => {
const user = userEvent.setup();
const onUiChange = vi.fn();
renderWithMantine(
<ProtocolScreen {...baseProps} onUiChange={onUiChange} />,
);
await user.click(screen.getByRole("button", { name: "Deselect All" }));
expect(onUiChange).toHaveBeenCalledWith(
expect.objectContaining({
visibleDirections: { client: false, server: false },
}),
);
});
it("emits the cleared method filter through onUiChange", async () => {
const user = userEvent.setup();
const onUiChange = vi.fn();
const { container } = renderWithMantine(
<ProtocolScreen
{...baseProps}
ui={{ ...EMPTY_PROTOCOL_UI, methodFilter: "tools/list" }}
onUiChange={onUiChange}
/>,
);
const clearButton = container.querySelector(
"button.mantine-InputClearButton-root",
) as HTMLButtonElement | null;
expect(clearButton).not.toBeNull();
await user.click(clearButton!);
expect(onUiChange).toHaveBeenCalledWith(
expect.objectContaining({ methodFilter: undefined }),
);
});
it("renders a pin-as-column button when onPin is provided and invokes it", async () => {
const user = userEvent.setup();
const onPin = vi.fn();
renderWithMantine(<ProtocolScreen {...baseProps} onPin={onPin} />);
await user.click(screen.getByRole("button", { name: "Pin as column" }));
expect(onPin).toHaveBeenCalledTimes(1);
});
it("drops the filter sidebar when embedded, keeping the request list", () => {
renderWithMantine(<ProtocolScreen {...baseProps} embedded />);
expect(screen.getByText("Messages")).toBeInTheDocument();
// The sidebar (ProtocolControls, with its Search box) is not rendered.
expect(screen.queryByPlaceholderText("Search...")).toBeNull();
});
it("applies the search text but ignores the method filter when embedded", () => {
renderWithMantine(
<ProtocolScreen
{...baseProps}
ui={{
...EMPTY_PROTOCOL_UI,
// Method filter would exclude the tools/list entries on the full-size
// screen...
methodFilter: "resources/list",
// ...but the column search matches them.
search: "tools",
}}
embedded
/>,
);
expect(screen.queryByText("No request history")).toBeNull();
});
it("hides entries not matching the column search when embedded", () => {
renderWithMantine(
<ProtocolScreen
{...baseProps}
ui={{ ...EMPTY_PROTOCOL_UI, search: "zzz-no-match" }}
embedded
/>,
);
expect(screen.getByText("No request history")).toBeInTheDocument();
});
});