Merge branch 'v2/main' into v2/chore/2156-use-mcp-apps
This commit is contained in:
commit
d79e96b0f2
13 changed files with 885 additions and 59 deletions
|
|
@ -91,6 +91,36 @@ The top-level `src/types/` is a separate sibling — ambient `.d.ts` module stub
|
|||
|
||||
Nothing _enforces_ the boundary — no path alias keys off it, and the coverage `include` in `vite.config.ts` lists both directories, so a move between them is coverage-neutral. It's a human-legible import-time signal. See [`AGENTS.md`](../../AGENTS.md) for the full rule (including the whitelist caveat — a module placed outside `components`/`lib`/`utils`/`server` falls out of the ≥90 gate).
|
||||
|
||||
## Core tab automation contract
|
||||
|
||||
The Tools, Resources, and Prompts screens each expose a `data-testid` plus a
|
||||
small set of `data-*` attributes, so a headless driver can `waitForSelector` on
|
||||
a deterministic signal rather than on visible copy. `scripts/smoke-web-tabs.mjs`
|
||||
drives all three against `test-servers/configs/web-tabs-http.json` ([#2148](https://github.com/modelcontextprotocol/inspector/issues/2148)).
|
||||
Treat them as a public contract, for the same reason as the Apps ones below:
|
||||
|
||||
| Attribute | Where | Meaning |
|
||||
| ------------------------------------ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `data-testid="tools-screen"` | Tools screen root | The element carrying the two attributes below. |
|
||||
| `data-tool-count` | on `tools-screen` | How many tools the list holds. `0` is a *populated screen with an empty list* — distinct from the screen being absent, which is what a driver waits out. |
|
||||
| `data-call-status` | on `tools-screen` | `idle` → `pending` → `ok` / `error` for the current `tools/call`. Always a value; an absent call state reads `idle`, never empty. |
|
||||
| `data-testid="structured-output"` | result panel | The `structuredContent` section of a tool result ([#1908](https://github.com/modelcontextprotocol/inspector/issues/1908)). Absent when a result carries none. |
|
||||
| `data-testid="resources-screen"` | Resources root | The element carrying the three attributes below. |
|
||||
| `data-resource-count` | on `resources-screen` | Entries from `resources/list`. |
|
||||
| `data-template-count` | on `resources-screen` | Entries from `resources/templates/list` — a **separate** call that can fail on its own, so it is reported separately. |
|
||||
| `data-read-status` | on `resources-screen` | `idle` → `pending` → `ok` / `error` for the current `resources/read`. |
|
||||
| `data-testid="resource-preview"` | preview panel | The read resource's **rendered** contents. Assert this *and* `data-read-status`: the status flips as soon as the RPC resolves, so it alone stays true with the panel removed. |
|
||||
| `data-testid="prompts-screen"` | Prompts root | The element carrying the two attributes below. |
|
||||
| `data-prompt-count` | on `prompts-screen`| Entries from `prompts/list`. |
|
||||
| `data-get-status` | on `prompts-screen`| `idle` → `pending` → `ok` / `error` for the current `prompts/get`. |
|
||||
| `data-testid="prompt-messages"` | messages panel | The fetched prompt's **rendered** messages — the `prompts/get` counterpart of `resource-preview`, and asserted alongside `data-get-status` for the same reason. |
|
||||
|
||||
Why attributes rather than text: a smoke that waited on a label fails the next
|
||||
time the label is reworded, which is noise rather than signal — and it fails as
|
||||
an opaque timeout, because there is nothing to compare against. The screen tests
|
||||
pin each attribute name for the same reason: a rename should fail loudly in a
|
||||
unit test, not silently in a five-minute gate.
|
||||
|
||||
## MCP Apps screen automation contract
|
||||
|
||||
The Apps screen exposes a small, stable set of `data-testid` / `data-*` attributes so an automated driver (deep-link auto-open, CI review harness) can `waitForSelector` on a deterministic signal instead of sleeping. Treat these as a public contract — drivers depend on them staying stable:
|
||||
|
|
|
|||
|
|
@ -64,3 +64,14 @@ describe("PromptMessagesDisplay", () => {
|
|||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// The `data-testid` the headless tab smoke waits on (#2148). It asserts the
|
||||
// messages RENDERED, which `data-get-status="ok"` alone does not — that flips
|
||||
// as soon as `prompts/get` resolves. Pinned here so a rename fails loudly in a
|
||||
// unit test rather than as an opaque timeout in a five-minute gate.
|
||||
describe("automation contract (#2148)", () => {
|
||||
it("exposes a stable data-testid on the panel", () => {
|
||||
renderWithMantine(<PromptMessagesDisplay messages={messages} />);
|
||||
expect(screen.getByTestId("prompt-messages")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -67,7 +67,10 @@ export function PromptMessagesDisplay({
|
|||
onClose,
|
||||
}: PromptMessagesDisplayProps) {
|
||||
return (
|
||||
<PanelStack>
|
||||
// `data-testid` so a driver can wait on the messages having actually
|
||||
// RENDERED, not merely on `prompts/get` having returned (#2148) — see the
|
||||
// ResourcePreviewPanel counterpart for why the RPC status is not enough.
|
||||
<PanelStack data-testid="prompt-messages">
|
||||
<HeaderRow>
|
||||
<HeaderLeft>
|
||||
{onClose && (
|
||||
|
|
|
|||
|
|
@ -566,3 +566,22 @@ describe("ResourcePreviewPanel", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
// The `data-testid` the headless tab smoke waits on (#2148) — see the
|
||||
// PromptMessagesDisplay counterpart: `data-read-status="ok"` flips as soon as
|
||||
// `resources/read` resolves, so only this proves the preview rendered.
|
||||
describe("automation contract (#2148)", () => {
|
||||
it("exposes a stable data-testid on the panel", () => {
|
||||
renderWithMantine(
|
||||
<ResourcePreviewPanel
|
||||
resource={textResource}
|
||||
contents={textContents}
|
||||
isSubscribed={false}
|
||||
onRefresh={vi.fn()}
|
||||
onSubscribe={vi.fn()}
|
||||
onUnsubscribe={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId("resource-preview")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -222,7 +222,12 @@ export function ResourcePreviewPanel({
|
|||
const sourceToggleable = isSourceToggleable(mimeType);
|
||||
|
||||
return (
|
||||
<PanelStack>
|
||||
// `data-testid` so a driver can wait on the preview having actually
|
||||
// RENDERED, not merely on `resources/read` having returned (#2148). The
|
||||
// read status flips as soon as the RPC resolves, so a smoke stopping there
|
||||
// would stay green with this panel removed. A `data-*` attribute is not
|
||||
// part of a component's typed props, so it is passed at the call site.
|
||||
<PanelStack data-testid="resource-preview">
|
||||
<HeaderRow>
|
||||
<HeaderLeft>
|
||||
{onClose && (
|
||||
|
|
|
|||
|
|
@ -85,3 +85,15 @@ describe("StructuredOutputPanel", () => {
|
|||
expect(screen.getByTestId("code-highlight")).toHaveTextContent("{}");
|
||||
});
|
||||
});
|
||||
|
||||
// The `data-testid` the headless tab smoke waits on (#2148). Asserting the
|
||||
// section by its heading copy would make the smoke fail on a rewording, so the
|
||||
// smoke keys off this attribute — pinned here so a rename fails loudly.
|
||||
describe("automation contract (#2148)", () => {
|
||||
it("exposes a stable data-testid on the section", () => {
|
||||
renderWithMantine(
|
||||
<StructuredOutputPanel structuredContent={structuredContent} />,
|
||||
);
|
||||
expect(screen.getByTestId("structured-output")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -72,7 +72,10 @@ export function StructuredOutputPanel({
|
|||
const [expanded, setExpanded] = useState(defaultExpanded);
|
||||
|
||||
return (
|
||||
<StructuredBox>
|
||||
// `data-testid` so the headless tab smoke (#2148) can assert this section
|
||||
// rendered without matching on its heading copy. A `data-*` attribute is
|
||||
// not part of a component's typed props, so it is passed at the call site.
|
||||
<StructuredBox data-testid="structured-output">
|
||||
<StructuredInner>
|
||||
<HeaderRow>
|
||||
<StructuredHeader>Structured Output</StructuredHeader>
|
||||
|
|
|
|||
|
|
@ -391,3 +391,34 @@ describe("PromptsScreen", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
// See the ToolsScreen counterpart: the `data-*` readiness contract the headless
|
||||
// tab smoke drives (#2148), pinned here so a rename fails loudly.
|
||||
describe("automation contract (#2148)", () => {
|
||||
it("reports the prompt count and an idle get status", () => {
|
||||
renderWithMantine(<PromptsScreen {...baseProps} />);
|
||||
const root = screen.getByTestId("prompts-screen");
|
||||
expect(root).toHaveAttribute(
|
||||
"data-prompt-count",
|
||||
String(promptsWithArgs.length),
|
||||
);
|
||||
expect(root).toHaveAttribute("data-get-status", "idle");
|
||||
});
|
||||
|
||||
it("tracks the get status through to a result", () => {
|
||||
renderWithMantine(
|
||||
<PromptsScreen
|
||||
{...baseProps}
|
||||
getPromptState={{
|
||||
status: "ok",
|
||||
promptName: promptsWithArgs[0].name,
|
||||
result: { messages: [] },
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId("prompts-screen")).toHaveAttribute(
|
||||
"data-get-status",
|
||||
"ok",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -274,7 +274,13 @@ export function PromptsScreen({
|
|||
}
|
||||
|
||||
return (
|
||||
<ScreenLayout>
|
||||
// `data-*` readiness contract for the headless tab smoke (#2148); see
|
||||
// clients/web/README.md#core-tab-automation-contract.
|
||||
<ScreenLayout
|
||||
data-testid="prompts-screen"
|
||||
data-prompt-count={prompts.length}
|
||||
data-get-status={getPromptState?.status ?? "idle"}
|
||||
>
|
||||
<Sidebar>
|
||||
<SidebarCard>
|
||||
<PromptControls
|
||||
|
|
|
|||
|
|
@ -403,3 +403,36 @@ describe("ResourcesScreen", () => {
|
|||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// See the ToolsScreen counterpart: the `data-*` readiness contract the headless
|
||||
// tab smoke drives (#2148), pinned here so a rename fails loudly.
|
||||
describe("automation contract (#2148)", () => {
|
||||
it("reports the resource and template counts separately", () => {
|
||||
renderWithMantine(<ResourcesScreen {...baseProps} />);
|
||||
const root = screen.getByTestId("resources-screen");
|
||||
// Two attributes rather than one: `resources/list` and
|
||||
// `resources/templates/list` are separate calls and either can fail alone.
|
||||
expect(root).toHaveAttribute(
|
||||
"data-resource-count",
|
||||
String(resources.length),
|
||||
);
|
||||
expect(root).toHaveAttribute(
|
||||
"data-template-count",
|
||||
String(templates.length),
|
||||
);
|
||||
expect(root).toHaveAttribute("data-read-status", "idle");
|
||||
});
|
||||
|
||||
it("tracks the read status through to a result", () => {
|
||||
renderWithMantine(
|
||||
<ResourcesScreen
|
||||
{...baseProps}
|
||||
readState={{ status: "ok", uri: "file:///x", result: okResult }}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId("resources-screen")).toHaveAttribute(
|
||||
"data-read-status",
|
||||
"ok",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -319,7 +319,14 @@ export function ResourcesScreen({
|
|||
}
|
||||
|
||||
return (
|
||||
<ScreenLayout>
|
||||
// `data-*` readiness contract for the headless tab smoke (#2148); see
|
||||
// clients/web/README.md#core-tab-automation-contract.
|
||||
<ScreenLayout
|
||||
data-testid="resources-screen"
|
||||
data-resource-count={resources.length}
|
||||
data-template-count={templates.length}
|
||||
data-read-status={readState?.status ?? "idle"}
|
||||
>
|
||||
<Sidebar>
|
||||
<SidebarCard>
|
||||
<ResourceControls
|
||||
|
|
|
|||
|
|
@ -445,3 +445,41 @@ describe("ToolsScreen", () => {
|
|||
expect(screen.getByText("invalid_header_tool")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// The `data-*` readiness contract the headless tab smoke drives (#2148). It is
|
||||
// a documented public contract (clients/web/README.md), and a smoke asserting
|
||||
// it fails as an opaque 45s timeout rather than a mismatch — so the attribute
|
||||
// names are pinned here, where a rename fails loudly instead.
|
||||
describe("automation contract (#2148)", () => {
|
||||
it("reports the tool count and an idle call status", () => {
|
||||
renderWithMantine(<ToolsScreen {...baseProps} />);
|
||||
const root = screen.getByTestId("tools-screen");
|
||||
expect(root).toHaveAttribute("data-tool-count", String(tools.length));
|
||||
// Absent call state reads `idle`, not empty: the smoke waits on a value.
|
||||
expect(root).toHaveAttribute("data-call-status", "idle");
|
||||
});
|
||||
|
||||
it("reports an empty list distinctly from a missing one", () => {
|
||||
renderWithMantine(<ToolsScreen {...baseProps} tools={[]} />);
|
||||
expect(screen.getByTestId("tools-screen")).toHaveAttribute(
|
||||
"data-tool-count",
|
||||
"0",
|
||||
);
|
||||
});
|
||||
|
||||
it("tracks the call status through to a result", () => {
|
||||
renderWithMantine(
|
||||
<ToolsScreen
|
||||
{...baseProps}
|
||||
callState={{
|
||||
status: "ok",
|
||||
result: { content: [{ type: "text", text: "done" }] },
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId("tools-screen")).toHaveAttribute(
|
||||
"data-call-status",
|
||||
"ok",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -198,7 +198,15 @@ export function ToolsScreen({
|
|||
};
|
||||
|
||||
return (
|
||||
<ScreenLayout>
|
||||
// `data-*` readiness contract for the headless tab smoke (#2148); see
|
||||
// clients/web/README.md#core-tab-automation-contract. Attributes are not
|
||||
// part of a component's typed props, so they are passed here rather than
|
||||
// baked into the `ScreenLayout` constant.
|
||||
<ScreenLayout
|
||||
data-testid="tools-screen"
|
||||
data-tool-count={tools.length}
|
||||
data-call-status={callState?.status ?? "idle"}
|
||||
>
|
||||
<Sidebar>
|
||||
<SidebarCard>
|
||||
<ToolControls
|
||||
|
|
|
|||
Loading…
Reference in a new issue