Extracts the Ace integration #2094 carried inline into a shared `elements/JsonEditor`, and renders it on every surface where the web client types or displays JSON. The element is deliberately text in, text out. The two editing contracts above it disagree about what an unparseable draft means — JsonObjectInput keeps the last valid object and says nothing to its parent, SchemaJsonField reports `undefined` and blocks submission (#2020) — so parsing, and the draft/value split that goes with it, stays with each caller. Surfaces: SchemaJsonField (inherited by all four SchemaForm consumers), a new "Edit as JSON" switch for the whole arguments object, the server.json File Contents box, the raw JSON-RPC request box, ContentViewer's JSON branch (read-only, inherited by its eleven consumers), and a new "Edit and replay" modal on a Protocol entry. Ace read-only replaces the CodeHighlight JSON path and the Prism `json` grammar loader is dropped: folding, not colorization, is what it adds, and two highlighters for one language drift. `wrap={false}` callers and untyped text that only looks like JSON keep the plain renderer. Closes #2151 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UL9retfvAXRgvi6EWk4SY4 Signed-off-by: cliffhall <cliff@futurescale.com>
376 lines
12 KiB
TypeScript
376 lines
12 KiB
TypeScript
import { describe, it, expect, vi } from "vitest";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
|
|
import { setAceText } from "../../../test/aceEditor";
|
|
import { ExperimentalFeaturesPanel } from "./ExperimentalFeaturesPanel";
|
|
import type { RequestHistoryItem } from "./ExperimentalFeaturesPanel";
|
|
|
|
const baseProps = {
|
|
serverExperimental: undefined,
|
|
clientExperimental: undefined,
|
|
requestDraft:
|
|
'{"jsonrpc":"2.0","id":1,"method":"experimental/x","params":{}}',
|
|
customHeaders: [],
|
|
requestHistory: [],
|
|
onToggleClientCapability: vi.fn(),
|
|
onRequestChange: vi.fn(),
|
|
onSendRequest: vi.fn(),
|
|
onAddHeader: vi.fn(),
|
|
onRemoveHeader: vi.fn(),
|
|
onHeaderChange: vi.fn(),
|
|
onCopyResponse: vi.fn(),
|
|
onTestCapability: vi.fn(),
|
|
};
|
|
|
|
describe("ExperimentalFeaturesPanel", () => {
|
|
it("renders the warning alert and section titles", () => {
|
|
renderWithMantine(<ExperimentalFeaturesPanel {...baseProps} />);
|
|
expect(
|
|
screen.getByText(
|
|
"These features are non-standard and may change or be removed.",
|
|
),
|
|
).toBeInTheDocument();
|
|
expect(
|
|
screen.getByText("Server Experimental Capabilities:"),
|
|
).toBeInTheDocument();
|
|
expect(
|
|
screen.getByText("Client Experimental Capabilities:"),
|
|
).toBeInTheDocument();
|
|
expect(screen.getByText("Advanced JSON-RPC Tester")).toBeInTheDocument();
|
|
});
|
|
|
|
it("shows the empty state when no server experimental capabilities are present", () => {
|
|
renderWithMantine(<ExperimentalFeaturesPanel {...baseProps} />);
|
|
expect(
|
|
screen.getByText("No experimental capabilities"),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders server capability cards with description and methods", () => {
|
|
renderWithMantine(
|
|
<ExperimentalFeaturesPanel
|
|
{...baseProps}
|
|
serverExperimental={{
|
|
"experimental/streaming": {
|
|
description: "Supports streaming responses",
|
|
methods: [
|
|
"experimental/stream.start",
|
|
"experimental/stream.cancel",
|
|
],
|
|
},
|
|
}}
|
|
/>,
|
|
);
|
|
expect(screen.getByText("experimental/streaming")).toBeInTheDocument();
|
|
expect(
|
|
screen.getByText("Supports streaming responses"),
|
|
).toBeInTheDocument();
|
|
expect(
|
|
screen.getByText(
|
|
"Methods: experimental/stream.start, experimental/stream.cancel",
|
|
),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders the capability card without description or methods when not provided", () => {
|
|
renderWithMantine(
|
|
<ExperimentalFeaturesPanel
|
|
{...baseProps}
|
|
serverExperimental={{
|
|
"experimental/bare": {},
|
|
}}
|
|
/>,
|
|
);
|
|
expect(screen.getByText("experimental/bare")).toBeInTheDocument();
|
|
expect(screen.queryByText(/^Methods:/)).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("invokes onTestCapability when Test is clicked on a capability card", async () => {
|
|
const user = userEvent.setup();
|
|
const onTestCapability = vi.fn();
|
|
renderWithMantine(
|
|
<ExperimentalFeaturesPanel
|
|
{...baseProps}
|
|
onTestCapability={onTestCapability}
|
|
serverExperimental={{
|
|
"experimental/echo": { description: "Echoes" },
|
|
}}
|
|
/>,
|
|
);
|
|
await user.click(screen.getByRole("button", { name: /Test/ }));
|
|
expect(onTestCapability).toHaveBeenCalledWith("experimental/echo");
|
|
});
|
|
|
|
it("renders the known client capability toggles with their friendly labels", () => {
|
|
renderWithMantine(<ExperimentalFeaturesPanel {...baseProps} />);
|
|
expect(
|
|
screen.getByRole("checkbox", { name: "Custom sampling" }),
|
|
).toBeInTheDocument();
|
|
expect(
|
|
screen.getByRole("checkbox", { name: "Batch requests" }),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
it("marks a known client capability checkbox as checked when enabled", () => {
|
|
renderWithMantine(
|
|
<ExperimentalFeaturesPanel
|
|
{...baseProps}
|
|
clientExperimental={{ "experimental/batchRequests": {} }}
|
|
/>,
|
|
);
|
|
const sampling = screen.getByRole("checkbox", {
|
|
name: "Custom sampling",
|
|
}) as HTMLInputElement;
|
|
const batch = screen.getByRole("checkbox", {
|
|
name: "Batch requests",
|
|
}) as HTMLInputElement;
|
|
expect(sampling.checked).toBe(false);
|
|
expect(batch.checked).toBe(true);
|
|
});
|
|
|
|
it("renders unknown client capabilities by their key", () => {
|
|
renderWithMantine(
|
|
<ExperimentalFeaturesPanel
|
|
{...baseProps}
|
|
clientExperimental={{
|
|
"experimental/customSampling": {},
|
|
"experimental/futureFeatureXyz": {},
|
|
}}
|
|
/>,
|
|
);
|
|
expect(
|
|
screen.getByRole("checkbox", { name: "experimental/futureFeatureXyz" }),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
it("invokes onToggleClientCapability when a client checkbox is clicked", async () => {
|
|
const user = userEvent.setup();
|
|
const onToggleClientCapability = vi.fn();
|
|
renderWithMantine(
|
|
<ExperimentalFeaturesPanel
|
|
{...baseProps}
|
|
onToggleClientCapability={onToggleClientCapability}
|
|
/>,
|
|
);
|
|
await user.click(screen.getByRole("checkbox", { name: "Custom sampling" }));
|
|
expect(onToggleClientCapability).toHaveBeenCalledWith(
|
|
"experimental/customSampling",
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("invokes onAddHeader when + Add Header is clicked", async () => {
|
|
const user = userEvent.setup();
|
|
const onAddHeader = vi.fn();
|
|
renderWithMantine(
|
|
<ExperimentalFeaturesPanel {...baseProps} onAddHeader={onAddHeader} />,
|
|
);
|
|
await user.click(screen.getByRole("button", { name: "+ Add Header" }));
|
|
expect(onAddHeader).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("renders custom header rows and invokes onHeaderChange when key changes", async () => {
|
|
const user = userEvent.setup();
|
|
const onHeaderChange = vi.fn();
|
|
renderWithMantine(
|
|
<ExperimentalFeaturesPanel
|
|
{...baseProps}
|
|
customHeaders={[{ key: "X-Auth", value: "Bearer 1" }]}
|
|
onHeaderChange={onHeaderChange}
|
|
/>,
|
|
);
|
|
const keyInput = screen.getByDisplayValue("X-Auth");
|
|
await user.type(keyInput, "Z");
|
|
expect(onHeaderChange).toHaveBeenCalled();
|
|
const lastCall =
|
|
onHeaderChange.mock.calls[onHeaderChange.mock.calls.length - 1];
|
|
expect(lastCall[0]).toBe(0);
|
|
expect(lastCall[2]).toBe("Bearer 1");
|
|
});
|
|
|
|
it("invokes onHeaderChange when value changes for a header", async () => {
|
|
const user = userEvent.setup();
|
|
const onHeaderChange = vi.fn();
|
|
renderWithMantine(
|
|
<ExperimentalFeaturesPanel
|
|
{...baseProps}
|
|
customHeaders={[{ key: "X-Auth", value: "abc" }]}
|
|
onHeaderChange={onHeaderChange}
|
|
/>,
|
|
);
|
|
const valueInput = screen.getByDisplayValue("abc");
|
|
await user.type(valueInput, "Z");
|
|
expect(onHeaderChange).toHaveBeenCalled();
|
|
const lastCall =
|
|
onHeaderChange.mock.calls[onHeaderChange.mock.calls.length - 1];
|
|
expect(lastCall[0]).toBe(0);
|
|
expect(lastCall[1]).toBe("X-Auth");
|
|
});
|
|
|
|
it("clears a header key via its clear button, invoking onHeaderChange with empty key", async () => {
|
|
const user = userEvent.setup();
|
|
const onHeaderChange = vi.fn();
|
|
renderWithMantine(
|
|
<ExperimentalFeaturesPanel
|
|
{...baseProps}
|
|
customHeaders={[{ key: "X-Auth", value: "Bearer 1" }]}
|
|
onHeaderChange={onHeaderChange}
|
|
/>,
|
|
);
|
|
// Both the key and value inputs render a Clear button when populated.
|
|
// The first one belongs to the key input (line 252).
|
|
const clearButtons = screen.getAllByRole("button", { name: "Clear" });
|
|
await user.click(clearButtons[0]);
|
|
expect(onHeaderChange).toHaveBeenCalledWith(0, "", "Bearer 1");
|
|
});
|
|
|
|
it("clears a header value via its clear button, invoking onHeaderChange with empty value", async () => {
|
|
const user = userEvent.setup();
|
|
const onHeaderChange = vi.fn();
|
|
renderWithMantine(
|
|
<ExperimentalFeaturesPanel
|
|
{...baseProps}
|
|
customHeaders={[{ key: "X-Auth", value: "Bearer 1" }]}
|
|
onHeaderChange={onHeaderChange}
|
|
/>,
|
|
);
|
|
// The second Clear button belongs to the value input (line 267).
|
|
const clearButtons = screen.getAllByRole("button", { name: "Clear" });
|
|
await user.click(clearButtons[1]);
|
|
expect(onHeaderChange).toHaveBeenCalledWith(0, "X-Auth", "");
|
|
});
|
|
|
|
it("clears the request draft via its clear button, invoking onRequestChange with empty string", async () => {
|
|
const user = userEvent.setup();
|
|
const onRequestChange = vi.fn();
|
|
renderWithMantine(
|
|
<ExperimentalFeaturesPanel
|
|
{...baseProps}
|
|
requestDraft='{"jsonrpc":"2.0"}'
|
|
onRequestChange={onRequestChange}
|
|
/>,
|
|
);
|
|
// The request textarea shows a Clear button when non-empty (line 292).
|
|
const clearButtons = screen.getAllByRole("button", { name: "Clear" });
|
|
await user.click(clearButtons[clearButtons.length - 1]);
|
|
expect(onRequestChange).toHaveBeenCalledWith("");
|
|
});
|
|
|
|
it("invokes onRemoveHeader with the header index when the remove icon is clicked", async () => {
|
|
const user = userEvent.setup();
|
|
const onRemoveHeader = vi.fn();
|
|
renderWithMantine(
|
|
<ExperimentalFeaturesPanel
|
|
{...baseProps}
|
|
customHeaders={[
|
|
{ key: "X-Auth", value: "abc" },
|
|
{ key: "X-Other", value: "def" },
|
|
]}
|
|
onRemoveHeader={onRemoveHeader}
|
|
/>,
|
|
);
|
|
const removeButtons = screen.getAllByRole("button", { name: "✕" });
|
|
await user.click(removeButtons[1]);
|
|
expect(onRemoveHeader).toHaveBeenCalledWith(1);
|
|
});
|
|
|
|
it("invokes onRequestChange when the request editor changes", async () => {
|
|
const onRequestChange = vi.fn();
|
|
renderWithMantine(
|
|
<ExperimentalFeaturesPanel
|
|
{...baseProps}
|
|
requestDraft=""
|
|
onRequestChange={onRequestChange}
|
|
/>,
|
|
);
|
|
await setAceText("x");
|
|
expect(onRequestChange).toHaveBeenCalledWith("x");
|
|
});
|
|
|
|
it("invokes onSendRequest when Send Request is clicked", async () => {
|
|
const user = userEvent.setup();
|
|
const onSendRequest = vi.fn();
|
|
renderWithMantine(
|
|
<ExperimentalFeaturesPanel
|
|
{...baseProps}
|
|
onSendRequest={onSendRequest}
|
|
/>,
|
|
);
|
|
await user.click(screen.getByRole("button", { name: "Send Request" }));
|
|
expect(onSendRequest).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("renders a successful response without an Error badge", () => {
|
|
renderWithMantine(
|
|
<ExperimentalFeaturesPanel
|
|
{...baseProps}
|
|
response={{
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
result: { ok: true },
|
|
}}
|
|
/>,
|
|
);
|
|
expect(screen.getByText("Response")).toBeInTheDocument();
|
|
expect(screen.queryByText("Error")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("renders an error response with the Error badge", () => {
|
|
renderWithMantine(
|
|
<ExperimentalFeaturesPanel
|
|
{...baseProps}
|
|
response={{
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
error: { code: -32601, message: "Method not found" },
|
|
}}
|
|
/>,
|
|
);
|
|
expect(screen.getByText("Response")).toBeInTheDocument();
|
|
expect(screen.getByText("Error")).toBeInTheDocument();
|
|
});
|
|
|
|
it("invokes onCopyResponse when Copy is clicked", async () => {
|
|
const user = userEvent.setup();
|
|
const onCopyResponse = vi.fn();
|
|
renderWithMantine(
|
|
<ExperimentalFeaturesPanel
|
|
{...baseProps}
|
|
response={{ jsonrpc: "2.0", id: 1, result: { ok: true } }}
|
|
onCopyResponse={onCopyResponse}
|
|
/>,
|
|
);
|
|
await user.click(screen.getByRole("button", { name: "Copy" }));
|
|
expect(onCopyResponse).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("renders the request history table when entries are present", () => {
|
|
const items: RequestHistoryItem[] = [
|
|
{
|
|
timestamp: new Date("2026-03-17T10:30:15Z"),
|
|
method: "experimental/metrics.get",
|
|
status: "success",
|
|
durationMs: 42,
|
|
},
|
|
{
|
|
timestamp: new Date("2026-03-17T10:29:50Z"),
|
|
method: "experimental/echo",
|
|
status: "error",
|
|
durationMs: 120,
|
|
},
|
|
];
|
|
renderWithMantine(
|
|
<ExperimentalFeaturesPanel {...baseProps} requestHistory={items} />,
|
|
);
|
|
expect(screen.getByText("Request History:")).toBeInTheDocument();
|
|
expect(screen.getByText("experimental/metrics.get")).toBeInTheDocument();
|
|
expect(screen.getByText("experimental/echo")).toBeInTheDocument();
|
|
expect(screen.getByText("42ms")).toBeInTheDocument();
|
|
expect(screen.getByText("120ms")).toBeInTheDocument();
|
|
});
|
|
|
|
it("does not render the history section when requestHistory is empty", () => {
|
|
renderWithMantine(<ExperimentalFeaturesPanel {...baseProps} />);
|
|
expect(screen.queryByText("Request History:")).not.toBeInTheDocument();
|
|
});
|
|
});
|