inspector/clients/web/src/components/groups/StructuredOutputPanel/StructuredOutputPanel.stories.tsx
cliffhall 8049b4f7c8 test: cover the structured-output cap and the list_items showcase end to end
Two gaps from the Copilot review on #1975:

- The `Large` story asserted nothing, so dropping the section's `mah` (or
  applying it to the wrong element) would still pass CI. Its play function
  now measures the real ScrollArea viewport: the visible height stays at
  the cap and the overflow is scrollable rather than clipped.

- Nothing exercised the `list_items` fixture, so a preset-registration
  typo would leave the documented showcase broken with a green suite. A
  new integration test loads `structured-output-http.json` through
  loadConfig/resolveConfig — covering the file, the registry entry, and
  the fixture — connects, calls the tool, and asserts both the summary
  block and the nested structuredContent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SW1p8E2uiyyLx4RKwrwSrt
2026-08-11 10:28:01 -04:00

109 lines
3.2 KiB
TypeScript

import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, userEvent, waitFor, within } from "storybook/test";
import { StructuredOutputPanel } from "./StructuredOutputPanel";
const nested = {
items: [
{ id: 1, name: "Item A", tags: ["foo", "bar"] },
{ id: 2, name: "Item B", tags: ["baz"] },
],
total: 2,
};
const flat = {
temperature: 25,
unit: "C",
city: "San Francisco",
};
// Long enough that the payload exceeds the section's max height and scrolls
// within it rather than pushing the rest of the result panel down.
const large = {
rows: Array.from({ length: 60 }, (_, index) => ({
id: index + 1,
label: `Row ${index + 1}`,
value: index * 7,
})),
total: 60,
};
const meta: Meta<typeof StructuredOutputPanel> = {
title: "Groups/StructuredOutputPanel",
component: StructuredOutputPanel,
};
export default meta;
type Story = StoryObj<typeof StructuredOutputPanel>;
export const Nested: Story = {
args: {
structuredContent: nested,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(
canvas.getByRole("heading", { name: "Structured Output" }),
).toBeInTheDocument();
// The section starts expanded, so the payload is visible without a click.
// Asserted on the container's text: once the Prism grammar loads, the JSON
// is split across per-token elements, so no single node holds the value.
await waitFor(() =>
expect(canvasElement.textContent).toContain('"Item A"'),
);
},
};
export const Flat: Story = {
args: {
structuredContent: flat,
},
};
// A payload taller than the section's cap scrolls inside it rather than
// pushing the rest of the result panel down. Asserted on the real geometry,
// so dropping `mah` (or moving it to the wrong element) fails here.
export const Large: Story = {
args: {
structuredContent: large,
},
play: async ({ canvasElement }) => {
await waitFor(() =>
expect(canvasElement.textContent).toContain('"Row 60"'),
);
const viewport = canvasElement.querySelector(
".mantine-ScrollArea-viewport",
);
if (!(viewport instanceof HTMLElement)) {
throw new Error("scroll viewport not found");
}
// Capped: the visible height stays at the section's `mah`, well under the
// payload's natural height…
expect(viewport.clientHeight).toBeLessThanOrEqual(400);
// …and the overflow is reachable by scrolling rather than clipped away.
expect(viewport.scrollHeight).toBeGreaterThan(viewport.clientHeight);
},
};
// Collapsing hides the payload; expanding brings it back.
export const Collapsed: Story = {
args: {
structuredContent: nested,
defaultExpanded: false,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const toggle = canvas.getByRole("button", {
name: "Expand structured output",
});
expect(toggle).toHaveAttribute("aria-expanded", "false");
await userEvent.click(toggle);
await waitFor(() =>
expect(
canvas.getByRole("button", { name: "Collapse structured output" }),
).toHaveAttribute("aria-expanded", "true"),
);
await waitFor(() =>
expect(canvasElement.textContent).toContain('"Item A"'),
);
},
};