/** * End-to-end tests for MCP Apps Activity Renderer * * Tests the complete flow of rendering MCP Apps UI: * 1. Activity snapshot received with resourceUri * 2. Resource fetched via proxied MCP request * 3. Sandboxed iframe created and communicates via JSON-RPC * 4. Tool calls proxied back through the agent */ import { fireEvent, screen, waitFor, act } from "@testing-library/react"; import { z } from "zod"; import { vi } from "vitest"; import { activitySnapshotEvent, renderWithCopilotKit, runFinishedEvent, runStartedEvent, testId, } from "../../../__tests__/utils/test-helpers"; import { MCPAppsActivityType, MCPAppsActivityContentSchema, } from "../../../components/MCPAppsActivityRenderer"; import { ReactActivityMessageRenderer } from "../../../types"; import { AbstractAgent, RunAgentInput, RunAgentResult, BaseEvent, EventType, } from "@ag-ui/client"; import { Observable, Subject } from "rxjs"; /** * Mock agent that intercepts runAgent calls for proxied MCP requests * while preserving normal streaming behavior for regular runs. */ class MockMCPProxyAgent extends AbstractAgent { private subject = new Subject(); // Track runAgent calls for verification public runAgentCalls: Array<{ input: Partial }> = []; // Configurable responses for proxied MCP requests private runAgentResponses: Map = new Map(); /** * Set the response for a specific MCP method */ setRunAgentResponse(method: string, response: unknown) { this.runAgentResponses.set(method, response); } /** * Emit a single agent event */ emit(event: BaseEvent) { if (event.type === EventType.RUN_STARTED) { this.isRunning = true; } else if ( event.type === EventType.RUN_FINISHED || event.type === EventType.RUN_ERROR ) { this.isRunning = false; } act(() => { this.subject.next(event); }); } /** * Complete the agent stream */ complete() { this.isRunning = false; act(() => { this.subject.complete(); }); } clone(): MockMCPProxyAgent { const cloned = new MockMCPProxyAgent(); cloned.agentId = this.agentId; type Internal = { subject: Subject; runAgentCalls: Array<{ input: Partial }>; runAgentResponses: Map; }; (cloned as unknown as Internal).subject = ( this as unknown as Internal ).subject; (cloned as unknown as Internal).runAgentCalls = ( this as unknown as Internal ).runAgentCalls; (cloned as unknown as Internal).runAgentResponses = ( this as unknown as Internal ).runAgentResponses; // Share isRunning with the original so that emit(runFinishedEvent()) on the // registry is visible to waitForAgentIdle() which now receives the clone. // // Also proxy runAgent dynamically so tests that monkey-patch agent.runAgent // after renderWithCopilotKit (which creates the clone) still take effect. // The clone is created and cached before tests can override runAgent, so a // static copy would always see the pre-patch prototype method. const registry = this; Object.defineProperty(cloned, "isRunning", { get() { return registry.isRunning; }, set(v: boolean) { registry.isRunning = v; }, configurable: true, enumerable: true, }); // Override runAgent so that: // - Proxied MCP requests delegate to registry.runAgent (picking up any // monkey-patches the test installed after renderWithCopilotKit). // - User-message runs call the prototype method with `this = clone` so // that clone.messages is updated (CopilotKit renders clone.messages). const proto = MockMCPProxyAgent.prototype; cloned.runAgent = async function ( input?: Partial, ): Promise { const proxiedRequest = input?.forwardedProps?.__proxiedMCPRequest; if (proxiedRequest) { // Delegate to the registry so that monkey-patches applied by tests // (e.g. "throws an error", "uses a controlled promise") take effect. return registry.runAgent(input); } // For user-message runs: call the prototype method bound to the clone // so AbstractAgent.runAgent processes events on clone and updates // clone.messages (which is what CopilotKit reads to render messages). return proto.runAgent.call(cloned, input); }; return cloned; } async detachActiveRun(): Promise {} run(_input: RunAgentInput): Observable { return this.subject.asObservable(); } /** * Override runAgent to intercept proxied MCP requests only. * For normal message flows, delegate to the parent class. */ async runAgent(input?: Partial): Promise { const proxiedRequest = input?.forwardedProps?.__proxiedMCPRequest as | { serverHash?: string; serverId?: string; method: string; params?: Record; } | undefined; // Only intercept proxied MCP requests if (proxiedRequest) { if (input) { this.runAgentCalls.push({ input }); } const method = proxiedRequest.method; const response = this.runAgentResponses.get(method); if (response !== undefined) { return { result: response, newMessages: [] }; } // Default responses if (method === "resources/read") { return { result: { contents: [ { uri: proxiedRequest.params?.uri, mimeType: "text/html", text: "Test content", }, ], }, newMessages: [], }; } if (method === "tools/call") { return { result: { content: [{ type: "text", text: "Tool call result" }], isError: false, }, newMessages: [], }; } return { result: {}, newMessages: [] }; } // For normal runs (user messages), use the parent's runAgent which // properly subscribes to run() and processes streaming events return super.runAgent(input); } } /** * Helper to create MCP Apps activity content matching the 0.0.2 schema */ function mcpAppsActivityContent(overrides: { resourceUri?: string; serverHash?: string; serverId?: string; toolInput?: Record; result?: { content?: unknown[]; structuredContent?: unknown; isError?: boolean; }; }) { return { resourceUri: overrides.resourceUri ?? "ui://test-server/test-resource", serverHash: overrides.serverHash ?? "abc123hash", serverId: overrides.serverId, toolInput: overrides.toolInput ?? {}, result: overrides.result ?? { content: [{ type: "text", text: "Tool output" }], isError: false, }, }; } describe("MCP Apps Activity Renderer E2E", () => { beforeEach(() => { // Reset any global state vi.clearAllMocks(); }); describe("Resource Fetching", () => { it("fetches resource content via proxied MCP request on mount", async () => { const agent = new MockMCPProxyAgent(); const agentId = "mcp-test-agent"; agent.agentId = agentId; // Set up response for resources/read agent.setRunAgentResponse("resources/read", { contents: [ { uri: "ui://test-server/dashboard", mimeType: "text/html", text: "Dashboard content", }, ], }); renderWithCopilotKit({ agents: { [agentId]: agent }, agentId, }); const input = await screen.findByRole("textbox"); fireEvent.change(input, { target: { value: "Show dashboard" } }); fireEvent.keyDown(input, { key: "Enter", code: "Enter" }); await waitFor(() => { expect(screen.getByText("Show dashboard")).toBeDefined(); }); const activityMessageId = testId("mcp-activity"); agent.emit(runStartedEvent()); agent.emit( activitySnapshotEvent({ messageId: activityMessageId, activityType: MCPAppsActivityType, content: mcpAppsActivityContent({ resourceUri: "ui://test-server/dashboard", serverHash: "dashboard-hash-123", }), }), ); agent.emit(runFinishedEvent()); // Wait for the activity renderer to mount and show loading await waitFor( () => { expect(screen.getByText("Loading...")).toBeDefined(); }, { timeout: 2000 }, ); // Wait for resource fetch to be called await waitFor( () => { expect(agent.runAgentCalls.length).toBeGreaterThan(0); }, { timeout: 2000 }, ); // Verify the proxied MCP request was made correctly const resourceCall = agent.runAgentCalls.find( (call) => call.input.forwardedProps?.__proxiedMCPRequest?.method === "resources/read", ); expect(resourceCall).toBeDefined(); expect( resourceCall?.input.forwardedProps?.__proxiedMCPRequest, ).toMatchObject({ serverHash: "dashboard-hash-123", method: "resources/read", params: { uri: "ui://test-server/dashboard" }, }); }); it("uses serverId when provided (takes precedence over serverHash)", async () => { const agent = new MockMCPProxyAgent(); const agentId = "mcp-test-agent"; agent.agentId = agentId; agent.setRunAgentResponse("resources/read", { contents: [ { uri: "ui://my-app/settings", mimeType: "text/html", text: "Settings", }, ], }); renderWithCopilotKit({ agents: { [agentId]: agent }, agentId, }); const input = await screen.findByRole("textbox"); fireEvent.change(input, { target: { value: "Show settings" } }); fireEvent.keyDown(input, { key: "Enter", code: "Enter" }); await waitFor(() => { expect(screen.getByText("Show settings")).toBeDefined(); }); agent.emit(runStartedEvent()); agent.emit( activitySnapshotEvent({ messageId: testId("mcp-activity"), activityType: MCPAppsActivityType, content: mcpAppsActivityContent({ resourceUri: "ui://my-app/settings", serverHash: "fallback-hash", serverId: "my-app-stable-id", // Should take precedence }), }), ); agent.emit(runFinishedEvent()); await waitFor(() => { const resourceCall = agent.runAgentCalls.find( (call) => call.input.forwardedProps?.__proxiedMCPRequest?.method === "resources/read", ); expect(resourceCall).toBeDefined(); expect( resourceCall?.input.forwardedProps?.__proxiedMCPRequest?.serverId, ).toBe("my-app-stable-id"); expect( resourceCall?.input.forwardedProps?.__proxiedMCPRequest?.serverHash, ).toBe("fallback-hash"); }); }); it("shows loading state while fetching resource", async () => { const agent = new MockMCPProxyAgent(); const agentId = "mcp-test-agent"; agent.agentId = agentId; // Create a promise that we can control let resolveResource: (value: unknown) => void; const resourcePromise = new Promise((resolve) => { resolveResource = resolve; }); // Override runAgent to use our controlled promise const originalRunAgent = agent.runAgent.bind(agent); agent.runAgent = async ( input?: Partial, ): Promise => { const proxiedRequest = input?.forwardedProps?.__proxiedMCPRequest as | { method: string } | undefined; if (proxiedRequest?.method === "resources/read") { await resourcePromise; return originalRunAgent(input); } return originalRunAgent(input); }; renderWithCopilotKit({ agents: { [agentId]: agent }, agentId, }); const input = await screen.findByRole("textbox"); fireEvent.change(input, { target: { value: "Load app" } }); fireEvent.keyDown(input, { key: "Enter", code: "Enter" }); await waitFor(() => { expect(screen.getByText("Load app")).toBeDefined(); }); agent.emit(runStartedEvent()); agent.emit( activitySnapshotEvent({ messageId: testId("mcp-activity"), activityType: MCPAppsActivityType, content: mcpAppsActivityContent({ resourceUri: "ui://test/app", serverHash: "test-hash", }), }), ); agent.emit(runFinishedEvent()); // Should show loading state await waitFor(() => { expect(screen.getByText("Loading...")).toBeDefined(); }); // Resolve the resource fetch act(() => { resolveResource!(true); }); // Loading should eventually disappear (iframe created) await waitFor( () => { expect(screen.queryByText("Loading...")).toBeNull(); }, { timeout: 3000 }, ); }); it("shows error state when resource fetch fails", async () => { const agent = new MockMCPProxyAgent(); const agentId = "mcp-test-agent"; agent.agentId = agentId; // Make proxied MCP requests throw an error const originalRunAgent = agent.runAgent.bind(agent); agent.runAgent = async ( input?: Partial, ): Promise => { const proxiedRequest = input?.forwardedProps?.__proxiedMCPRequest as | { method: string } | undefined; if (proxiedRequest) { throw new Error("Network error: Failed to fetch resource"); } return originalRunAgent(input); }; renderWithCopilotKit({ agents: { [agentId]: agent }, agentId, }); const input = await screen.findByRole("textbox"); fireEvent.change(input, { target: { value: "Fetch broken" } }); fireEvent.keyDown(input, { key: "Enter", code: "Enter" }); await waitFor(() => { expect(screen.getByText("Fetch broken")).toBeDefined(); }); agent.emit(runStartedEvent()); agent.emit( activitySnapshotEvent({ messageId: testId("mcp-activity"), activityType: MCPAppsActivityType, content: mcpAppsActivityContent({ resourceUri: "ui://broken/resource", serverHash: "broken-hash", }), }), ); agent.emit(runFinishedEvent()); // Should show error state await waitFor(() => { expect( screen.getByText(/Error:.*Failed to fetch resource/i), ).toBeDefined(); }); }); it("handles resource with no content gracefully", async () => { const agent = new MockMCPProxyAgent(); const agentId = "mcp-test-agent"; agent.agentId = agentId; // Return empty contents agent.setRunAgentResponse("resources/read", { contents: [], }); renderWithCopilotKit({ agents: { [agentId]: agent }, agentId, }); const input = await screen.findByRole("textbox"); fireEvent.change(input, { target: { value: "Empty resource" } }); fireEvent.keyDown(input, { key: "Enter", code: "Enter" }); await waitFor(() => { expect(screen.getByText("Empty resource")).toBeDefined(); }); agent.emit(runStartedEvent()); agent.emit( activitySnapshotEvent({ messageId: testId("mcp-activity"), activityType: MCPAppsActivityType, content: mcpAppsActivityContent({ resourceUri: "ui://empty/resource", serverHash: "empty-hash", }), }), ); agent.emit(runFinishedEvent()); // Should show error about no content await waitFor(() => { expect(screen.getByText(/Error:.*No resource content/i)).toBeDefined(); }); }); }); describe("Schema Validation", () => { it("validates activity content with the correct schema", () => { // Valid content const validContent = { resourceUri: "ui://server/resource", serverHash: "hash123", result: { content: [{ type: "text", text: "output" }], isError: false, }, }; const validResult = MCPAppsActivityContentSchema.safeParse(validContent); expect(validResult.success).toBe(true); // With optional serverId const withServerId = { ...validContent, serverId: "stable-server-id", }; const serverIdResult = MCPAppsActivityContentSchema.safeParse(withServerId); expect(serverIdResult.success).toBe(true); // With toolInput const withToolInput = { ...validContent, toolInput: { param1: "value1", param2: 42 }, }; const toolInputResult = MCPAppsActivityContentSchema.safeParse(withToolInput); expect(toolInputResult.success).toBe(true); }); it("rejects invalid activity content", () => { // Missing required fields const missingResourceUri = { serverHash: "hash123", result: { isError: false }, }; expect( MCPAppsActivityContentSchema.safeParse(missingResourceUri).success, ).toBe(false); const missingServerHash = { resourceUri: "ui://server/resource", result: { isError: false }, }; expect( MCPAppsActivityContentSchema.safeParse(missingServerHash).success, ).toBe(false); const missingResult = { resourceUri: "ui://server/resource", serverHash: "hash123", }; expect( MCPAppsActivityContentSchema.safeParse(missingResult).success, ).toBe(false); }); }); describe("Activity Type Integration", () => { it("built-in MCP Apps renderer is registered with correct activity type", async () => { const agent = new MockMCPProxyAgent(); const agentId = "mcp-test-agent"; agent.agentId = agentId; renderWithCopilotKit({ agents: { [agentId]: agent }, agentId, // Don't pass any custom renderers - built-in should be used }); const input = await screen.findByRole("textbox"); fireEvent.change(input, { target: { value: "Test MCP" } }); fireEvent.keyDown(input, { key: "Enter", code: "Enter" }); await waitFor(() => { expect(screen.getByText("Test MCP")).toBeDefined(); }); agent.emit(runStartedEvent()); agent.emit( activitySnapshotEvent({ messageId: testId("mcp-activity"), activityType: "mcp-apps", // Should match MCPAppsActivityType content: mcpAppsActivityContent({ resourceUri: "ui://builtin/test", serverHash: "builtin-hash", }), }), ); agent.emit(runFinishedEvent()); // Should show loading (meaning the renderer was matched) await waitFor(() => { expect(screen.getByText("Loading...")).toBeDefined(); }); }); it("user-provided renderer takes precedence over built-in", async () => { const agent = new MockMCPProxyAgent(); const agentId = "mcp-test-agent"; agent.agentId = agentId; // Custom renderer that overrides the built-in const customRenderer: ReactActivityMessageRenderer< z.infer > = { activityType: MCPAppsActivityType, content: MCPAppsActivityContentSchema, render: ({ content }) => (
Custom MCP Renderer: {content.resourceUri}
), }; renderWithCopilotKit({ agents: { [agentId]: agent }, agentId, renderActivityMessages: [customRenderer], }); const input = await screen.findByRole("textbox"); fireEvent.change(input, { target: { value: "Custom renderer" } }); fireEvent.keyDown(input, { key: "Enter", code: "Enter" }); await waitFor(() => { expect(screen.getByText("Custom renderer")).toBeDefined(); }); agent.emit(runStartedEvent()); agent.emit( activitySnapshotEvent({ messageId: testId("mcp-activity"), activityType: MCPAppsActivityType, content: mcpAppsActivityContent({ resourceUri: "ui://custom/resource", serverHash: "custom-hash", }), }), ); agent.emit(runFinishedEvent()); // Should render custom component, not loading await waitFor(() => { expect(screen.getByTestId("custom-mcp-renderer")).toBeDefined(); expect( screen.getByText(/Custom MCP Renderer:.*ui:\/\/custom\/resource/), ).toBeDefined(); }); }); }); describe("Multiple Activity Messages", () => { it("renders multiple MCP Apps activities independently", async () => { const agent = new MockMCPProxyAgent(); const agentId = "mcp-test-agent"; agent.agentId = agentId; // Set up different responses for different URIs const originalRunAgent = agent.runAgent.bind(agent); agent.runAgent = async ( input?: Partial, ): Promise => { const proxiedRequest = input?.forwardedProps?.__proxiedMCPRequest as { method: string; params?: { uri?: string }; }; if (proxiedRequest?.method === "resources/read") { const uri = proxiedRequest.params?.uri; if (uri === "ui://first/app") { return { result: { contents: [ { uri, mimeType: "text/html", text: "
First App
" }, ], }, newMessages: [], }; } if (uri === "ui://second/app") { return { result: { contents: [ { uri, mimeType: "text/html", text: "
Second App
" }, ], }, newMessages: [], }; } } // For non-proxied requests, use original behavior return originalRunAgent(input); }; renderWithCopilotKit({ agents: { [agentId]: agent }, agentId, }); const input = await screen.findByRole("textbox"); fireEvent.change(input, { target: { value: "Multiple apps" } }); fireEvent.keyDown(input, { key: "Enter", code: "Enter" }); await waitFor(() => { expect(screen.getByText("Multiple apps")).toBeDefined(); }); agent.emit(runStartedEvent()); // Emit two activity messages agent.emit( activitySnapshotEvent({ messageId: testId("mcp-first"), activityType: MCPAppsActivityType, content: mcpAppsActivityContent({ resourceUri: "ui://first/app", serverHash: "first-hash", }), }), ); agent.emit( activitySnapshotEvent({ messageId: testId("mcp-second"), activityType: MCPAppsActivityType, content: mcpAppsActivityContent({ resourceUri: "ui://second/app", serverHash: "second-hash", }), }), ); agent.emit(runFinishedEvent()); // Both activities should trigger resource fetches and create iframes. // Due to async timing, the loading states might clear quickly, // so we verify both iframes are eventually created. await waitFor( () => { const iframes = document.querySelectorAll("iframe[srcdoc]"); expect(iframes.length).toBe(2); }, { timeout: 2000 }, ); }); }); describe("Content Types", () => { it("handles text content from resource", async () => { const agent = new MockMCPProxyAgent(); const agentId = "mcp-test-agent"; agent.agentId = agentId; agent.setRunAgentResponse("resources/read", { contents: [ { uri: "ui://test/text", mimeType: "text/html", text: "

Text Content

", }, ], }); renderWithCopilotKit({ agents: { [agentId]: agent }, agentId, }); const input = await screen.findByRole("textbox"); fireEvent.change(input, { target: { value: "Text content" } }); fireEvent.keyDown(input, { key: "Enter", code: "Enter" }); await waitFor(() => { expect(screen.getByText("Text content")).toBeDefined(); }); agent.emit(runStartedEvent()); agent.emit( activitySnapshotEvent({ messageId: testId("mcp-activity"), activityType: MCPAppsActivityType, content: mcpAppsActivityContent({ resourceUri: "ui://test/text", serverHash: "text-hash", }), }), ); agent.emit(runFinishedEvent()); // Should transition from loading to rendered await waitFor(() => { expect(screen.getByText("Loading...")).toBeDefined(); }); }); it("handles blob (base64) content from resource", async () => { const agent = new MockMCPProxyAgent(); const agentId = "mcp-test-agent"; agent.agentId = agentId; // Base64 encoded "Blob Content" const base64Html = btoa("Blob Content"); agent.setRunAgentResponse("resources/read", { contents: [ { uri: "ui://test/blob", mimeType: "text/html", blob: base64Html, }, ], }); renderWithCopilotKit({ agents: { [agentId]: agent }, agentId, }); const input = await screen.findByRole("textbox"); fireEvent.change(input, { target: { value: "Blob content" } }); fireEvent.keyDown(input, { key: "Enter", code: "Enter" }); await waitFor(() => { expect(screen.getByText("Blob content")).toBeDefined(); }); agent.emit(runStartedEvent()); agent.emit( activitySnapshotEvent({ messageId: testId("mcp-activity"), activityType: MCPAppsActivityType, content: mcpAppsActivityContent({ resourceUri: "ui://test/blob", serverHash: "blob-hash", }), }), ); agent.emit(runFinishedEvent()); // Should show loading initially await waitFor(() => { expect(screen.getByText("Loading...")).toBeDefined(); }); }); it("handles resource with no text or blob - iframe created but stuck waiting for sandbox", async () => { // NOTE: In jsdom, the sandbox iframe (using srcdoc) can't fully execute, so the // component will create an iframe and wait for the sandbox proxy to be ready. // The actual error for missing text/blob happens inside the sandbox communication // flow which can't complete in jsdom. This test verifies that: // 1. The component fetches the resource successfully // 2. The iframe is created (showing the component progressed past loading) const agent = new MockMCPProxyAgent(); const agentId = "mcp-test-agent"; agent.agentId = agentId; // Resource with neither text nor blob agent.setRunAgentResponse("resources/read", { contents: [ { uri: "ui://test/empty", mimeType: "text/html", // No text or blob field - in real environment this would cause an error // after sandbox proxy is ready, but in jsdom the proxy never responds }, ], }); renderWithCopilotKit({ agents: { [agentId]: agent }, agentId, }); const input = await screen.findByRole("textbox"); fireEvent.change(input, { target: { value: "No content" } }); fireEvent.keyDown(input, { key: "Enter", code: "Enter" }); await waitFor(() => { expect(screen.getByText("No content")).toBeDefined(); }); agent.emit(runStartedEvent()); agent.emit( activitySnapshotEvent({ messageId: testId("mcp-activity"), activityType: MCPAppsActivityType, content: mcpAppsActivityContent({ resourceUri: "ui://test/empty", serverHash: "empty-hash", }), }), ); agent.emit(runFinishedEvent()); // Verify the iframe is created (component progressed past loading) // In jsdom, the sandbox proxy never responds, so the error for missing text/blob // is never reached. This is a limitation of jsdom testing. await waitFor( () => { const iframe = document.querySelector("iframe[srcdoc]"); expect(iframe).not.toBeNull(); }, { timeout: 2000 }, ); }); }); describe("Metadata Handling", () => { it("applies border styling when prefersBorder is true", async () => { const agent = new MockMCPProxyAgent(); const agentId = "mcp-test-agent"; agent.agentId = agentId; agent.setRunAgentResponse("resources/read", { contents: [ { uri: "ui://test/bordered", mimeType: "text/html", text: "Bordered Content", _meta: { ui: { prefersBorder: true, }, }, }, ], }); renderWithCopilotKit({ agents: { [agentId]: agent }, agentId, }); const input = await screen.findByRole("textbox"); fireEvent.change(input, { target: { value: "Bordered app" } }); fireEvent.keyDown(input, { key: "Enter", code: "Enter" }); await waitFor(() => { expect(screen.getByText("Bordered app")).toBeDefined(); }); agent.emit(runStartedEvent()); agent.emit( activitySnapshotEvent({ messageId: testId("mcp-activity"), activityType: MCPAppsActivityType, content: mcpAppsActivityContent({ resourceUri: "ui://test/bordered", serverHash: "bordered-hash", }), }), ); agent.emit(runFinishedEvent()); // Wait for resource to be fetched and iframe to be created await waitFor( () => { // Loading should disappear expect(screen.queryByText("Loading...")).toBeNull(); // Iframe should be created const iframe = document.querySelector("iframe[srcdoc]"); expect(iframe).not.toBeNull(); }, { timeout: 3000 }, ); // Note: Border styling is applied via inline styles based on prefersBorder metadata. // In jsdom, verifying inline styles is not reliable, but we've verified the component // renders successfully with the metadata that includes prefersBorder: true. }); it("does not apply border styling when prefersBorder is false", async () => { const agent = new MockMCPProxyAgent(); const agentId = "mcp-test-agent"; agent.agentId = agentId; agent.setRunAgentResponse("resources/read", { contents: [ { uri: "ui://test/borderless", mimeType: "text/html", text: "Borderless Content", _meta: { ui: { prefersBorder: false, }, }, }, ], }); renderWithCopilotKit({ agents: { [agentId]: agent }, agentId, }); const input = await screen.findByRole("textbox"); fireEvent.change(input, { target: { value: "Borderless app" } }); fireEvent.keyDown(input, { key: "Enter", code: "Enter" }); await waitFor(() => { expect(screen.getByText("Borderless app")).toBeDefined(); }); agent.emit(runStartedEvent()); agent.emit( activitySnapshotEvent({ messageId: testId("mcp-activity"), activityType: MCPAppsActivityType, content: mcpAppsActivityContent({ resourceUri: "ui://test/borderless", serverHash: "borderless-hash", }), }), ); agent.emit(runFinishedEvent()); // Verify component renders without error await waitFor(() => { expect(screen.getByText("Loading...")).toBeDefined(); }); }); }); });