The web client had exactly one connected-flow smoke and it was the Apps tab. `smoke:web:browser` proves the bundle boots and paints but never connects; `smoke:web:app` / `smoke:web:elicit` connect but only drive the Apps surface. So the three highest-traffic tabs were unexercised through the prod bundle end to end — everything covering them (unit tests, Storybook, the integration project) mocks or bypasses the prod server, so a regression living only in the built bundle or in the prod Hono backend's wiring of a tab passed all of them. `scripts/smoke-web-tabs.mjs` drives all three against one server on one browser launch (~5s added wall-clock): Tools runs `list_items` and asserts its `structuredContent` section rendered (#1908), Resources reads `events` with the template list asserted separately — `resources/list` and `resources/templates/list` are different calls and either can fail alone — and Prompts fetches the argument-less `simple_prompt`. Every assertion is on a `data-*` attribute, never on copy, mirroring the `data-app-status` precedent: each screen now carries a readiness contract (`data-tool-count` / `data-call-status` and its Resources / Prompts equivalents), tabulated in the web README and pinned by screen unit tests so a rename fails loudly there rather than as an opaque timeout here. The deep-link connect step is extracted to `lib/deep-link-connect.mjs` rather than copied a third time. That matters because `parseDeepLink` ignores a link it cannot validate rather than reporting one, so a drifted copy fails as an opaque connect timeout in whichever smoke was not updated. `smoke:web:elicit`'s hand-rolled copy is retired onto it. Chromium-only and deliberately not in `ENGINE_SMOKES`: the engine tiers exist for the MCP Apps sandbox's genuinely divergent primitives, and these tabs are ordinary React and Mantine. Network, Protocol, Logs and Tasks remain uncovered — a stated gap, not an oversight. Closes #2148 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>
99 lines
3.6 KiB
TypeScript
99 lines
3.6 KiB
TypeScript
import { describe, it, expect, vi } from "vitest";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
|
|
import { StructuredOutputPanel } from "./StructuredOutputPanel";
|
|
|
|
// The real CodeHighlight dynamic-imports the Prism runtime and each grammar
|
|
// (its own behavior is covered in CodeHighlight.test.tsx). Stub it so these
|
|
// tests assert on the JSON handed to it, synchronously.
|
|
vi.mock("../../elements/CodeHighlight/CodeHighlight", () => ({
|
|
CodeHighlight: ({ language, code }: { language: string; code: string }) => (
|
|
<pre data-testid="code-highlight" data-language={language}>
|
|
{code}
|
|
</pre>
|
|
),
|
|
}));
|
|
|
|
const structuredContent = {
|
|
items: [
|
|
{ id: 1, name: "Item A", tags: ["foo", "bar"] },
|
|
{ id: 2, name: "Item B", tags: ["baz"] },
|
|
],
|
|
total: 2,
|
|
};
|
|
|
|
describe("StructuredOutputPanel", () => {
|
|
it("renders a labeled section with the payload as pretty-printed JSON", () => {
|
|
renderWithMantine(
|
|
<StructuredOutputPanel structuredContent={structuredContent} />,
|
|
);
|
|
expect(
|
|
screen.getByRole("heading", { name: "Structured Output" }),
|
|
).toBeInTheDocument();
|
|
const code = screen.getByTestId("code-highlight");
|
|
expect(code).toHaveAttribute("data-language", "json");
|
|
expect(code).toHaveTextContent(/"total": 2/);
|
|
// Nested values are inspectable field by field, not summarized away.
|
|
expect(code).toHaveTextContent(/"name": "Item A"/);
|
|
expect(code).toHaveTextContent(/"tags"/);
|
|
});
|
|
|
|
it("starts expanded by default", () => {
|
|
renderWithMantine(
|
|
<StructuredOutputPanel structuredContent={structuredContent} />,
|
|
);
|
|
expect(
|
|
screen.getByRole("button", { name: "Collapse structured output" }),
|
|
).toHaveAttribute("aria-expanded", "true");
|
|
});
|
|
|
|
it("starts collapsed when defaultExpanded is false", () => {
|
|
renderWithMantine(
|
|
<StructuredOutputPanel
|
|
structuredContent={structuredContent}
|
|
defaultExpanded={false}
|
|
/>,
|
|
);
|
|
expect(
|
|
screen.getByRole("button", { name: "Expand structured output" }),
|
|
).toHaveAttribute("aria-expanded", "false");
|
|
});
|
|
|
|
it("toggles between expanded and collapsed", async () => {
|
|
const user = userEvent.setup();
|
|
renderWithMantine(
|
|
<StructuredOutputPanel structuredContent={structuredContent} />,
|
|
);
|
|
await user.click(
|
|
screen.getByRole("button", { name: "Collapse structured output" }),
|
|
);
|
|
const toggle = screen.getByRole("button", {
|
|
name: "Expand structured output",
|
|
});
|
|
expect(toggle).toHaveAttribute("aria-expanded", "false");
|
|
await user.click(toggle);
|
|
expect(
|
|
screen.getByRole("button", { name: "Collapse structured output" }),
|
|
).toHaveAttribute("aria-expanded", "true");
|
|
});
|
|
|
|
it("renders an empty structured payload rather than nothing", () => {
|
|
renderWithMantine(<StructuredOutputPanel structuredContent={{}} />);
|
|
expect(
|
|
screen.getByRole("heading", { name: "Structured Output" }),
|
|
).toBeInTheDocument();
|
|
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();
|
|
});
|
|
});
|