87 lines · 3.7 KB · TypeScript
RawDownload
import { test, expect } from "sunpeak/test"; // executor's create-artifact shell mounts the generated component in a *nested* srcdoc// iframe (shell-app -> inner-renderer, the MCP-Apps double-iframe sandbox), one// level below sunpeak's own `result.app()`. Descend that extra level.const appBody = (result: { app: () => ReturnType<ReturnType<typeof test>["app"]> } | any) =>  result.app().frameLocator("iframe"); const COUNTER = `function App() {  const [count, setCount] = useState(0);  return (    <div className="p-4">      <h2>MCP App counter</h2>      <Badge>{count}</Badge>      <Button onClick={() => setCount((c) => c + 1)}>Increment</Button>    </div>  );}`; // sunpeak runs every test against both the Claude and ChatGPT host simulations. test("create-artifact mounts an interactive React widget", async ({ inspector }) => {  const result = await inspector.renderTool("create-artifact", {    code: COUNTER,    title: "MCP App counter",  });  const app = appBody(result);   await expect(app.locator("text=MCP App counter")).toBeVisible({ timeout: 15000 });  const inc = app.locator('button:has-text("Increment")');  await expect(inc).toBeVisible();   // state is live: clicking updates the rendered output  await inc.click();  await expect(app.locator("text=1")).toBeVisible();}); test("create-artifact renders in dark theme", async ({ inspector }) => {  const result = await inspector.renderTool(    "create-artifact",    { code: COUNTER, title: "MCP App counter" },    { theme: "dark" },  );  await expect(appBody(result).locator("text=MCP App counter")).toBeVisible({ timeout: 15000 });}); // The whole point of patching sunpeak to advertise MCP-Apps support is that// executor returns the inline widget rather than its deep-link fallback. Assert// that directly, so a future sunpeak change that drops the capability fails// here instead of quietly testing the wrong delivery path.// Protocol-level assertions go through the `mcp` fixture: `inspector` models// what a *host* shows the user and does not surface the raw tool result.const structuredOf = (result: { structuredContent?: unknown }): Record<string, unknown> =>  (result.structuredContent ?? {}) as Record<string, unknown>; test("create-artifact returns an inline widget, not the deep-link fallback", async ({ mcp }) => {  const result = await mcp.callTool("create-artifact", {    code: COUNTER,    title: "MCP App counter",    description: "A counter used by the MCP Apps e2e harness",  });  const structured = structuredOf(result);  // The whole point of the inspector advertising MCP-Apps support: executor  // must hand back the component, not a URL to go look at it elsewhere.  expect(structured.status).not.toBe("fallback_url");  expect(structured.code).toContain("MCP App counter");  // Every successful render is persisted, so the artifact can be reopened.  expect(structured.artifactId).toBeTruthy();}); test("a rendered artifact is listed and can be shown again", async ({ inspector, mcp }) => {  const rendered = await mcp.callTool("create-artifact", {    code: COUNTER,    title: "Retrievable counter",    description: "Saved so list-artifacts can find it",  });  const artifactId = structuredOf(rendered).artifactId as string;  expect(artifactId).toBeTruthy();   const listed = await mcp.callTool("list-artifacts", {});  const artifacts = structuredOf(listed).artifacts as Array<{ id: string }> | undefined;  expect(artifacts?.some((artifact) => artifact.id === artifactId)).toBe(true);   // …and the stored source really does render, through the same shell.  const shown = await inspector.renderTool("show-artifact", { id: artifactId });  await expect(appBody(shown).locator("text=MCP App counter")).toBeVisible({ timeout: 15000 });});