Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/dojo/e2e/aimock-setup.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { registerClaudeSdkWeatherFixtures } from "./claude-sdk-weather-fixtures";
import { registerGenerativeUIFixtures } from "./generative-ui-fixtures";
import {
LLMock,
type ChatCompletionRequest,
Expand Down Expand Up @@ -98,6 +99,9 @@ export function registerLLMockFixtures(mockServer: LLMock): void {
// The interrupt/subgraphs legs go first: their prompts ("San Francisco",
// meeting bookings) would otherwise hit the backend-tool-rendering legs and
// Mastra's schedule_meeting fixture. Each requires one of their own tools.
// Generative UI (A2UI 1.0): scoped to a unique prompt and to the guide in
// the system prompt, so they claim no other demo's turn.
registerGenerativeUIFixtures(mockServer);
registerAntigravityInterruptFixtures(mockServer);
registerAntigravityFixtures(mockServer);
registerAntigravityChatFixtures(mockServer);
Expand Down
42 changes: 42 additions & 0 deletions apps/dojo/e2e/featurePages/GenerativeUIPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Page, Locator, expect } from "@playwright/test";
import { CopilotSelectors } from "../utils/copilot-selectors";
import { sendChatMessage } from "../utils/copilot-actions";

/** The generative UI demos: send a request, then check the rendered surface. */
export class GenerativeUIPage {
readonly page: Page;
readonly chatInput: Locator;
readonly surface: Locator;

constructor(page: Page) {
this.page = page;
this.chatInput = CopilotSelectors.chatTextarea(page);
this.surface = page.locator("[data-generative-ui-root]");
}

async sendMessage(message: string) {
await sendChatMessage(this.page, message);
}

/**
* The plan picker rendered: its title, the seat slider and the bound
* total. The title may be lifted out of the surface into the message's
* prose (markdown transport), so it is looked up in the whole chat.
*/
async assertPlanPickerRendered() {
await expect(this.surface.first()).toBeVisible({ timeout: 60_000 });
await expect(this.page.getByText("Team plan").first()).toBeVisible();
const slider = this.surface.getByRole("slider", { name: "Seats" });
await expect(slider).toBeVisible();
await expect(this.page.getByText("5 seats selected")).toBeVisible();
// The total follows the slider with no round trip to the agent.
await slider.focus();
await slider.press("ArrowRight");
await expect(this.page.getByText("6 seats selected")).toBeVisible();
}

/** No surface failed to render. */
async assertNoRenderErrors() {
await expect(this.page.locator("[data-generative-ui-error]")).toHaveCount(0);
}
}
133 changes: 133 additions & 0 deletions apps/dojo/e2e/generative-ui-fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import type { ChatCompletionRequest, LLMock } from "@copilotkit/aimock";
import { textOf } from "./lib/fixture-message-text";

/**
* Generative UI (A2UI 1.0) fixtures. CopilotKit sends its guide as context
* entries; these fixtures answer only when that context reached the model's
* request, so a passing test proves the integration injected it.
*
* - Markdown transport: the guide starts "Generative UI markdown guide".
* - Tool transport: the guide starts "Generative UI guide", and the request
* offers the render_ui tool the runtime adds.
*/
export const GENERATIVE_UI_PROMPT = "Show me a team plan picker";

/**
* Every message's text. Adapters put the context in different places: the
* system prompt (Claude Agent SDK, AG2), a transient model hook (Strands)
* or the run's message context (Agno). Wherever it lands, the guide can only
* be in the request if the adapter forwarded RunAgentInput.context.
*/
const requestText = (req: ChatCompletionRequest) =>
req.messages.map((m) => textOf(m.content)).join("\n");

const lastUserText = (req: ChatCompletionRequest) =>
textOf(req.messages.filter((m) => m.role === "user").at(-1)?.content);

/**
* The render_ui tool as the model sees it. Some adapters prefix frontend
* tools (the Claude Agent SDK serves them as MCP tools, "mcp__…__render_ui").
*/
const renderUITool = (req: ChatCompletionRequest) =>
req.tools?.find(
(t) => t.function.name === "render_ui" || t.function.name.endsWith("__render_ui"),
)?.function.name;

/**
* The user's prompt is the latest turn. System messages after it are skipped:
* CopilotKit's LangGraph middleware appends its context message there.
*/
const asksForPlanPicker = (req: ChatCompletionRequest) =>
req.messages.filter((m) => m.role !== "system").at(-1)?.role === "user" &&
lastUserText(req).includes(GENERATIVE_UI_PROMPT);

/** The surface both transports render: a title, a seat slider and a bound total. */
export const PLAN_PICKER_OPERATIONS = [
{
version: "v1.0",
createSurface: {
surfaceId: "plan-picker",
dataModel: { seats: 5 },
components: [
{ id: "root", component: "Column", children: ["title", "seats", "total"] },
],
},
},
{
version: "v1.0",
updateComponents: {
surfaceId: "plan-picker",
components: [
{ id: "title", component: "Title", text: "Team plan" },
{
id: "seats",
component: "Slider",
label: "Seats",
min: 1,
max: 50,
value: { "@path": "/seats" },
},
{
id: "total",
component: "Text",
text: {
"@call": "formatString",
args: { value: "${/seats} seats selected" },
},
},
],
},
},
];

export function registerGenerativeUIFixtures(server: LLMock) {
// Markdown transport: the reply carries the operations in an a2ui fence.
server.addFixture({
match: {
predicate: (req) =>
asksForPlanPicker(req) &&
requestText(req).includes("Generative UI markdown guide"),
},
response: {
content: [
"Here is a plan picker.",
"",
"```a2ui",
...PLAN_PICKER_OPERATIONS.map((op) => JSON.stringify(op)),
"```",
].join("\n"),
},
});

// Tool transport: one render_ui call with the operations.
server.addFixture({
match: {
predicate: (req) =>
asksForPlanPicker(req) &&
renderUITool(req) !== undefined &&
requestText(req).includes("Generative UI guide"),
},
response: (req) => ({
toolCalls: [
{
name: renderUITool(req) ?? "render_ui",
arguments: JSON.stringify({
target: "chat",
operations: PLAN_PICKER_OPERATIONS,
}),
id: "call_render_plan_picker",
},
],
}),
});

// The turn after the render_ui result, when the agent loops back.
server.addFixture({
match: {
predicate: (req) =>
req.messages.at(-1)?.role === "tool" &&
lastUserText(req).includes(GENERATIVE_UI_PROMPT),
},
response: { content: "The plan picker is above: move the slider to pick seats." },
});
}
15 changes: 15 additions & 0 deletions apps/dojo/e2e/tests/adkJsTests/generativeUIPage.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { test } from "../../test-isolation-helper";
import { GenerativeUIPage } from "../../featurePages/GenerativeUIPage";
import { GENERATIVE_UI_PROMPT } from "../../generative-ui-fixtures";

for (const transport of ["markdown", "tool"] as const) {
test(`[ADK JavaScript] Generative UI (${transport}) renders the A2UI surface from the injected guide`, async ({
page,
}) => {
await page.goto(`/adk-js/feature/generative_ui_${transport}`);
const genui = new GenerativeUIPage(page);
await genui.sendMessage(GENERATIVE_UI_PROMPT);
await genui.assertPlanPickerRendered();
await genui.assertNoRenderErrors();
});
}
15 changes: 15 additions & 0 deletions apps/dojo/e2e/tests/adkMiddlewareTests/generativeUIPage.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { test } from "../../test-isolation-helper";
import { GenerativeUIPage } from "../../featurePages/GenerativeUIPage";
import { GENERATIVE_UI_PROMPT } from "../../generative-ui-fixtures";

for (const transport of ["markdown", "tool"] as const) {
test(`[ADK Middleware] Generative UI (${transport}) renders the A2UI surface from the injected guide`, async ({
page,
}) => {
await page.goto(`/adk-middleware/feature/generative_ui_${transport}`);
const genui = new GenerativeUIPage(page);
await genui.sendMessage(GENERATIVE_UI_PROMPT);
await genui.assertPlanPickerRendered();
await genui.assertNoRenderErrors();
});
}
15 changes: 15 additions & 0 deletions apps/dojo/e2e/tests/ag2Tests/generativeUIPage.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { test } from "../../test-isolation-helper";
import { GenerativeUIPage } from "../../featurePages/GenerativeUIPage";
import { GENERATIVE_UI_PROMPT } from "../../generative-ui-fixtures";

for (const transport of ["markdown", "tool"] as const) {
test(`[AG2] Generative UI (${transport}) renders the A2UI surface from the injected guide`, async ({
page,
}) => {
await page.goto(`/ag2/feature/generative_ui_${transport}`);
const genui = new GenerativeUIPage(page);
await genui.sendMessage(GENERATIVE_UI_PROMPT);
await genui.assertPlanPickerRendered();
await genui.assertNoRenderErrors();
});
}
15 changes: 15 additions & 0 deletions apps/dojo/e2e/tests/agUiDotnetTests/generativeUIPage.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { test } from "../../test-isolation-helper";
import { GenerativeUIPage } from "../../featurePages/GenerativeUIPage";
import { GENERATIVE_UI_PROMPT } from "../../generative-ui-fixtures";

for (const transport of ["markdown", "tool"] as const) {
test(`[AG-UI .NET SDK] Generative UI (${transport}) renders the A2UI surface from the injected guide`, async ({
page,
}) => {
await page.goto(`/ag-ui-dotnet/feature/generative_ui_${transport}`);
const genui = new GenerativeUIPage(page);
await genui.sendMessage(GENERATIVE_UI_PROMPT);
await genui.assertPlanPickerRendered();
await genui.assertNoRenderErrors();
});
}
15 changes: 15 additions & 0 deletions apps/dojo/e2e/tests/agnoTests/generativeUIPage.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { test } from "../../test-isolation-helper";
import { GenerativeUIPage } from "../../featurePages/GenerativeUIPage";
import { GENERATIVE_UI_PROMPT } from "../../generative-ui-fixtures";

for (const transport of ["markdown", "tool"] as const) {
test(`[Agno] Generative UI (${transport}) renders the A2UI surface from the injected guide`, async ({
page,
}) => {
await page.goto(`/agno/feature/generative_ui_${transport}`);
const genui = new GenerativeUIPage(page);
await genui.sendMessage(GENERATIVE_UI_PROMPT);
await genui.assertPlanPickerRendered();
await genui.assertNoRenderErrors();
});
}
15 changes: 15 additions & 0 deletions apps/dojo/e2e/tests/awsStrandsTests/generativeUIPage.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { test } from "../../test-isolation-helper";
import { GenerativeUIPage } from "../../featurePages/GenerativeUIPage";
import { GENERATIVE_UI_PROMPT } from "../../generative-ui-fixtures";

for (const transport of ["markdown", "tool"] as const) {
test(`[AWS Strands] Generative UI (${transport}) renders the A2UI surface from the injected guide`, async ({
page,
}) => {
await page.goto(`/aws-strands/feature/generative_ui_${transport}`);
const genui = new GenerativeUIPage(page);
await genui.sendMessage(GENERATIVE_UI_PROMPT);
await genui.assertPlanPickerRendered();
await genui.assertNoRenderErrors();
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { test } from "../../test-isolation-helper";
import { GenerativeUIPage } from "../../featurePages/GenerativeUIPage";
import { GENERATIVE_UI_PROMPT } from "../../generative-ui-fixtures";

for (const transport of ["markdown", "tool"] as const) {
test(`[AWS Strands TypeScript] Generative UI (${transport}) renders the A2UI surface from the injected guide`, async ({
page,
}) => {
await page.goto(`/aws-strands-typescript/feature/generative_ui_${transport}`);
const genui = new GenerativeUIPage(page);
await genui.sendMessage(GENERATIVE_UI_PROMPT);
await genui.assertPlanPickerRendered();
await genui.assertNoRenderErrors();
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { test } from "../../test-isolation-helper";
import { GenerativeUIPage } from "../../featurePages/GenerativeUIPage";
import { GENERATIVE_UI_PROMPT } from "../../generative-ui-fixtures";

for (const transport of ["markdown", "tool"] as const) {
test(`[Claude Agent SDK Python] Generative UI (${transport}) renders the A2UI surface from the injected guide`, async ({
page,
}) => {
await page.goto(`/claude-agent-sdk-python/feature/generative_ui_${transport}`);
const genui = new GenerativeUIPage(page);
await genui.sendMessage(GENERATIVE_UI_PROMPT);
await genui.assertPlanPickerRendered();
await genui.assertNoRenderErrors();
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { test } from "../../test-isolation-helper";
import { GenerativeUIPage } from "../../featurePages/GenerativeUIPage";
import { GENERATIVE_UI_PROMPT } from "../../generative-ui-fixtures";

for (const transport of ["markdown", "tool"] as const) {
test(`[Claude Agent SDK Typescript] Generative UI (${transport}) renders the A2UI surface from the injected guide`, async ({
page,
}) => {
await page.goto(`/claude-agent-sdk-typescript/feature/generative_ui_${transport}`);
const genui = new GenerativeUIPage(page);
await genui.sendMessage(GENERATIVE_UI_PROMPT);
await genui.assertPlanPickerRendered();
await genui.assertNoRenderErrors();
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { test } from "../../test-isolation-helper";
import { GenerativeUIPage } from "../../featurePages/GenerativeUIPage";
import { GENERATIVE_UI_PROMPT } from "../../generative-ui-fixtures";

for (const transport of ["markdown", "tool"] as const) {
test(`[Cloudflare Agents] Generative UI (${transport}) renders the A2UI surface from the injected guide`, async ({
page,
}) => {
await page.goto(`/cloudflare-agents/feature/generative_ui_${transport}`);
const genui = new GenerativeUIPage(page);
await genui.sendMessage(GENERATIVE_UI_PROMPT);
await genui.assertPlanPickerRendered();
await genui.assertNoRenderErrors();
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ const parityFeatures = [
"a2ui_dynamic_schema",
"a2ui_recovery",
"a2ui_fixed_schema",
"generative_ui_markdown",
"generative_ui_tool",
] as const;

test.describe("CrewAI Conversational Flows feature parity", () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { test } from "../../test-isolation-helper";
import { GenerativeUIPage } from "../../featurePages/GenerativeUIPage";
import { GENERATIVE_UI_PROMPT } from "../../generative-ui-fixtures";

for (const transport of ["markdown", "tool"] as const) {
test(`[CrewAI Conversational Flows] Generative UI (${transport}) renders the A2UI surface from the injected guide`, async ({
page,
}) => {
await page.goto(`/crewai-conversational-flows/feature/generative_ui_${transport}`);
const genui = new GenerativeUIPage(page);
await genui.sendMessage(GENERATIVE_UI_PROMPT);
await genui.assertPlanPickerRendered();
await genui.assertNoRenderErrors();
});
}
15 changes: 15 additions & 0 deletions apps/dojo/e2e/tests/crewAITests/generativeUIPage.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { test } from "../../test-isolation-helper";
import { GenerativeUIPage } from "../../featurePages/GenerativeUIPage";
import { GENERATIVE_UI_PROMPT } from "../../generative-ui-fixtures";

for (const transport of ["markdown", "tool"] as const) {
test(`[CrewAI] Generative UI (${transport}) renders the A2UI surface from the injected guide`, async ({
page,
}) => {
await page.goto(`/crewai/feature/generative_ui_${transport}`);
const genui = new GenerativeUIPage(page);
await genui.sendMessage(GENERATIVE_UI_PROMPT);
await genui.assertPlanPickerRendered();
await genui.assertNoRenderErrors();
});
}
Loading
Loading